> ## Documentation Index
> Fetch the complete documentation index at: https://docs.snagsolutions.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Create loyalty rules using API

> Learn how to create different types of loyalty rules through the API with the correct parameters and metadata

<head>
  <script type="application/ld+json">
    {JSON.stringify({
            '@context': 'https://schema.org',
            '@graph': [
              {
                '@type': 'TechArticle',
                headline: 'Create loyalty rules using API',
                description:
                  'Learn how to create different types of loyalty rules through the API with the correct parameters and metadata',
                author: {
                  '@type': 'Organization',
                  name: 'Snag Solutions',
                  url: 'https://www.snagsolutions.io/',
                },
                publisher: {
                  '@type': 'Organization',
                  name: 'Snag Solutions',
                  url: 'https://www.snagsolutions.io/',
                  logo: {
                    '@type': 'ImageObject',
                    url: 'https://assets.snagsolutions.io/public/docs/snag-logo-dark-no-bg.svg',
                  },
                },
                mainEntityOfPage:
                  'https://docs.snagsolutions.io/loyalty/create-rule-using-api',
              },
              {
                '@type': 'BreadcrumbList',
                itemListElement: [
                  {
                    '@type': 'ListItem',
                    position: 1,
                    name: 'Home',
                    item: 'https://docs.snagsolutions.io/welcome',
                  },
                  {
                    '@type': 'ListItem',
                    position: 2,
                    name: 'Loyalty',
                    item: 'https://docs.snagsolutions.io/loyalty/loyalty-overview',
                  },
                  {
                    '@type': 'ListItem',
                    position: 3,
                    name: 'Advanced workflows',
                    item: 'https://docs.snagsolutions.io/loyalty/managing-user-accounts',
                  },
                  { '@type': 'ListItem', position: 4, name: 'Create rule using API' },
                ],
              },
            ],
          })}
  </script>
</head>

<Tip>
  **Using an AI coding assistant?** Connect Cursor, Claude, or Copilot to Snag Docs for context-aware help. [Learn how](/ai-coding-assistant).
</Tip>

## 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:

<ParamField path="organizationId" type="string" required>
  The organization ID that owns this rule.
</ParamField>

<ParamField path="websiteId" type="string" required>
  The website ID where this rule will be applied.
</ParamField>

<ParamField path="name" type="string" required>
  The name of the loyalty rule (max 255 characters).
</ParamField>

<ParamField path="description" type="string">
  Optional description of the rule.
</ParamField>

<ParamField path="type" type="LoyaltyRuleType" required>
  The type of loyalty rule. See available types below.
</ParamField>

<ParamField path="frequency" type="LoyaltyRuleFrequency" required>
  How often eligibility is checked. Options: `once`, `daily`, `weekly`,
  `monthly`, `immediately`.
</ParamField>

<ParamField path="interval" type="LoyaltyRuleInterval">
  How often users can earn rewards. Options: `once`, `daily`, `weekly`,
  `monthly`, `unlimited`, `custom`.
</ParamField>

<ParamField path="startTime" type="ISO8601 string" required>
  When the rule becomes active (ISO 8601 format).
</ParamField>

<ParamField path="endTime" type="ISO8601 string">
  Optional end time for the rule (ISO 8601 format).
</ParamField>

<ParamField path="rewardType" type="LoyaltyRuleRewardType" required>
  Type of reward: `points`, `multiplier`, or `badge`.
</ParamField>

<ParamField path="loyaltyCurrencyId" type="string" required>
  The ID of the loyalty currency (points) to reward.
</ParamField>

<ParamField path="network" type="NetworkType" required>
  The blockchain network. Options: `mainnet`, `goerli`, `polygon`, `arbitrum`,
  etc.
</ParamField>

## Reward Configuration

### Reward Types

<Tabs>
  <Tab title="Points">
    Reward users with points for completing actions.

    <ResponseField name="amount" type="number" required>
      The number of points to reward.
    </ResponseField>

    <ResponseField name="loyaltyCurrencyId" type="string" required>
      ID of the currency to reward with.
    </ResponseField>
  </Tab>

  <Tab title="Multiplier">
    Reward users with a multiplier that enhances their earnings.

    <ResponseField name="amount" type="number" required>
      The multiplier value (must be > 1).
    </ResponseField>

    <ResponseField name="metadata.isRetroactive" type="boolean">
      Apply multiplier to user's existing balance.
    </ResponseField>

    <ResponseField name="metadata.multiplierLoyaltyCurrencyIds" type="Array<string>">
      Array of currency IDs to apply multiplier to. Empty array applies to all currencies.
    </ResponseField>
  </Tab>

  <Tab title="Badge">
    Reward users with a badge.

    <ResponseField name="loyaltyBadgeId" type="string" required>
      ID of the badge to reward.
    </ResponseField>

    <ResponseField name="rewardLifetime" type="LoyaltyRuleRewardLifetime">
      How long the badge lasts: `permanent` or `dynamic`.
    </ResponseField>
  </Tab>
</Tabs>

## 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.

<ResponseField name="metadata.completeProfileConditions" type="Record<string, boolean>">
  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).
</ResponseField>

### Refer a Friend (`referred_user`)

Reward users for referring others.

<ResponseField name="amount" type="number" required>
  Points for the referred user.
</ResponseField>

<ResponseField name="metadata.referrerReward" type="number" required>
  Points for the referrer.
</ResponseField>

<ResponseField name="metadata.referrerRewardLoyaltyCurrencyId" type="string">
  Currency for referrer reward.
</ResponseField>

<ResponseField name="metadata.referralRequirements.achievePoints" type="boolean">
  Require referred users to achieve points.
</ResponseField>

<ResponseField name="metadata.referralRequirements.points.amount" type="number">
  Required points when achievePoints is true.
</ResponseField>

<ResponseField name="metadata.referralRequirements.completeProfile" type="boolean">
  Require complete profile.
</ResponseField>

<ResponseField name="metadata.referralRequirements.connectTwitter" type="boolean">
  Require Twitter connection.
</ResponseField>

<ResponseField name="metadata.referralRequirements.connectDiscord" type="boolean">
  Require Discord connection.
</ResponseField>

<ResponseField name="metadata.referralRequirements.connectEmail" type="boolean">
  Require email connection.
</ResponseField>

### Check In (`check_in`)

Reward users for checking in.

<ResponseField name="amount" type="number" required>
  Points for each check-in.
</ResponseField>

<ResponseField name="metadata.enableStreaks" type="boolean">
  Enable streak tracking for consecutive check-ins.
</ResponseField>

<ResponseField name="metadata.streakArray" type="Array<Streak>">
  Optional array of streak milestones and rewards.
</ResponseField>

<Accordion title="Streak Configuration">
  <ResponseField name="streakMilestone" type="number" required>
    Number of consecutive days for this streak.
  </ResponseField>

  <ResponseField name="streakAmount" type="number" required>
    Points awarded for reaching this milestone.
  </ResponseField>
</Accordion>

## 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`)

<ResponseField name="metadata.enableVerifiedMultiplier" type="boolean">
  Apply a higher reward to verified (blue-check) accounts.
</ResponseField>

<ResponseField name="metadata.verifiedMultiplier" type="number">
  Multiplier applied when `enableVerifiedMultiplier` is true (e.g. `1.5`).
</ResponseField>

### Connect Discord (`connected_discord`)

<ResponseField name="metadata.enableJoinDiscordServers" type="boolean">
  Require users to also join specific Discord servers.
</ResponseField>

<ResponseField name="metadata.discordServersToJoin" type="Array<DiscordServer>">
  Servers (and optionally channels/roles) users must join.
</ResponseField>

<Accordion title="Discord Server Structure">
  <ResponseField name="id" type="string" required>
    Discord server ID.
  </ResponseField>

  <ResponseField name="channels" type="Array<Channel>">
    Channels to verify activity in.
  </ResponseField>

  <ResponseField name="roles" type="Array<Role>">
    Optional roles to verify.
  </ResponseField>
</Accordion>

### Connect Wallet (`connect_wallet`)

<ResponseField name="metadata.walletType" type="WalletType" required>
  Which wallet type the user must connect (e.g. `evm`, `solana`).
</ResponseField>

## 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`)

<ResponseField name="metadata.twitterPostUrl" type="string" required>
  URL of the X post to engage with.
</ResponseField>

<ResponseField name="metadata.twitterUserId" type="string" required>
  Twitter user ID of the post author.
</ResponseField>

<ResponseField name="metadata.checkLike" type="boolean">
  Require liking the post.
</ResponseField>

<ResponseField name="metadata.checkRepost" type="boolean">
  Require reposting.
</ResponseField>

<ResponseField name="metadata.checkComment" type="boolean">
  Require commenting.
</ResponseField>

<ResponseField name="oauthCredentialsId" type="string">
  Twitter OAuth credentials (top-level field).
</ResponseField>

### Post on X (`drip_x_new_tweet`)

<ResponseField name="metadata.checkText" type="string | Array<string>">
  Text that must be included in the tweet.
</ResponseField>

<ResponseField name="metadata.requirePostLink" type="boolean">
  Require users to submit post link (default: true).
</ResponseField>

<ResponseField name="metadata.requirePostMediaLink" type="boolean">
  Require post to include media.
</ResponseField>

### Follow an X Account (`drip_x_follow`, `twitter_follow`)

<ResponseField name="metadata.twitterUsername" type="string" required>
  X account to follow.
</ResponseField>

<ResponseField name="metadata.twitterUserId" type="string" required>
  Twitter user ID of the account.
</ResponseField>

<ResponseField name="oauthCredentialsId" type="string" required>
  OAuth credentials for Twitter (top-level field).
</ResponseField>

<Info>
  `drip_x_follow` accepts the account as `metadata.twitterAccountUrl` (full
  profile URL) instead of username/ID.
</Info>

### Add Text to X Bio / Username / Comment (`drip_x_text_in_bio`, `drip_x_text_in_name`, `drip_x_text_in_comment`)

<ResponseField name="metadata.checkText" type="string | Array<string>" required>
  Text that must appear in the user's bio, display name, or comment.
</ResponseField>

<ResponseField name="metadata.rewardQualityPosts" type="boolean">
  (`drip_x_text_in_comment`) Only reward posts that pass quality/sentiment
  checks.
</ResponseField>

<ResponseField name="metadata.minimumFollowerCount" type="number">
  (`drip_x_text_in_comment`) Minimum follower count to qualify.
</ResponseField>

### Reach X Followers (`twitter_followers`)

<ResponseField name="metadata.range" type="Array<RewardAmountRange>" required>
  Tiered rewards by the user's follower count.
</ResponseField>

### Get X Post Impressions (`post_impressions`)

<ResponseField name="metadata.range" type="Array<RewardAmountRange>" required>
  Tiered rewards by post view/impression count.
</ResponseField>

<ResponseField name="metadata.rewardQualityPosts" type="boolean">
  Only reward posts that pass quality/sentiment checks.
</ResponseField>

<ResponseField name="metadata.minimumFollowerCount" type="number">
  Minimum follower count to qualify.
</ResponseField>

<ResponseField name="trackProgress" type="boolean">
  Optional top-level flag to track progress. `mediaUrl` (top-level) can supply a
  media reference.
</ResponseField>

### Repost an X Post (`Retweet`)

<ResponseField name="metadata.twitterUserId" type="string" required>
  Twitter user ID of the account whose posts are monitored for reposts.
</ResponseField>

### Like an X Post (`twitter_like`)

<ResponseField name="metadata.twitterUsername" type="string" required>
  Account that posted the tweet.
</ResponseField>

<ResponseField name="metadata.twitterUserId" type="string" required>
  Twitter user ID.
</ResponseField>

<ResponseField name="oauthCredentialsId" type="string" required>
  OAuth credentials (top-level field).
</ResponseField>

### Comment on an X Post (`twitter_comment`)

<ResponseField name="metadata.twitterUserId" type="string" required>
  Twitter user ID of the account whose posts are monitored for comments.
</ResponseField>

### Post on X with Hashtag (`twitter_post_hashtag`)

<ResponseField name="metadata.twitterHashtag" type="string" required>
  Hashtag to search for in recent posts.
</ResponseField>

### Get an X Post Liked by Project (`tweet_liked_by_project`)

<ResponseField name="metadata.twitterUserId" type="string" required>
  Twitter user ID of the project account whose like qualifies the user.
</ResponseField>

<ResponseField name="oauthCredentialsId" type="string" required>
  OAuth credentials for Twitter (top-level field).
</ResponseField>

<ResponseField name="metadata.rewardPerImpression" type="boolean">
  Multiply the reward by the tweet's impression count.
</ResponseField>

### Get Discord Role (`discord_member`)

<ResponseField name="metadata.discordServersToJoin" type="Array<DiscordServer>" required>
  Servers and roles to verify. Each item uses `id` and `roles`.
</ResponseField>

### Join Discord Server (`discord_join`)

<ResponseField name="metadata.discordServersToJoin" type="Array<DiscordServer>" required>
  Servers the user must join. Only the server `id` is required for this type.
</ResponseField>

### Send Discord Messages (`DiscordMessages`)

<ResponseField name="metadata.discordServersToJoin" type="Array<DiscordServer>" required>
  Server `id` and the `channels` whose messages count. Each channel can specify
  `text` to match and `emojis` reactions.
</ResponseField>

<ResponseField name="metadata.range" type="Array<RewardAmountRange>">
  Optional tiered rewards based on message count.
</ResponseField>

### Grant a Discord Role (`discord_role_grant`)

<ResponseField name="metadata.discordServersToJoin" type="Array<DiscordServer>" required>
  Server `id` plus the `roles` to grant. `roles` is required for this type.
</ResponseField>

<Accordion title="discordServersToJoin item structure">
  <ResponseField name="id" type="string" required>
    Discord server (guild) ID.
  </ResponseField>

  <ResponseField name="channels" type="Array<Channel>">
    Channels to check. Each: `{ id: string, text?: string, emojis?: Array<{ id: string }> }`. Used by `DiscordMessages`.
  </ResponseField>

  <ResponseField name="roles" type="Array<{ id: string }>">
    Roles to verify or grant. Required for `discord_role_grant`.
  </ResponseField>
</Accordion>

### Join Telegram Group (`telegram_join`)

<ResponseField name="metadata.telegramChannelId" type="string" required>
  ID of the Telegram channel the user must join.
</ResponseField>

### Send Telegram Messages (`telegram_messages`)

<ResponseField name="metadata.telegramChannelId" type="string" required>
  ID of the Telegram channel whose messages count.
</ResponseField>

<ResponseField name="metadata.range" type="Array<RewardAmountRange>">
  Optional tiered rewards based on message count.
</ResponseField>

### Follow a TikTok Account (`tiktok_follow`)

<ResponseField name="metadata.twitterAccountUrl" type="string" required>
  TikTok profile URL to follow (e.g. `https://www.tiktok.com/@username`).
  Despite the field name, this holds the TikTok URL.
</ResponseField>

### Post on TikTok (`tiktok_post`)

<ResponseField name="metadata.checkText" type="string | Array<string>">
  Text to check in post.
</ResponseField>

<ResponseField name="metadata.requirePostLink" type="boolean">
  Require post link submission.
</ResponseField>

<ResponseField name="metadata.rewardCriteria" type="LoyaltyPostRewardCriteria">
  `IMPRESSIONS_COUNT` or `ELIGIBLE_POST`.
</ResponseField>

### Post on Instagram (`instagram_post`)

<ResponseField name="metadata.checkText" type="string | Array<string>" required>
  Text or hashtag the post must include.
</ResponseField>

<ResponseField name="metadata.rewardQualityPosts" type="boolean">
  Only reward posts that pass quality checks.
</ResponseField>

<ResponseField name="metadata.minimumFollowerCount" type="number">
  Minimum follower count to qualify.
</ResponseField>

### Follow an Instagram Account (`instagram_follow`)

<ResponseField name="metadata.twitterAccountUrl" type="string" required>
  Instagram profile URL to follow (e.g. `https://www.instagram.com/username`).
  Despite the field name, this holds the Instagram URL.
</ResponseField>

### Subscribe to a YouTube Channel (`youtube_subscribers`)

<ResponseField name="metadata.youtubeChannelId" type="string" required>
  YouTube channel ID the user must subscribe to.
</ResponseField>

<ResponseField name="externalIntegrationId" type="string">
  Optional Google API integration ID (top-level field) used for verification.
</ResponseField>

### Comment on a YouTube Video (`youtube_comment`)

<ResponseField name="metadata.youtubeVideoId" type="string" required>
  YouTube video ID the user must comment on.
</ResponseField>

<ResponseField name="externalIntegrationId" type="string">
  Optional Google API integration ID (top-level field) used for verification.
</ResponseField>

### Comment on a Reddit Post (`reddit_comment`)

<ResponseField name="metadata.redditPostId" type="string" required>
  ID of the Reddit post users must comment on.
</ResponseField>

<ResponseField name="metadata.cta.href" type="string" required>
  Full URL of the Reddit post.
</ResponseField>

### Add an Item to Steam Wishlist (`steam_wishlist`)

<ResponseField name="metadata.steamAppId" type="string" required>
  Steam application ID the user must wishlist.
</ResponseField>

### Spend in Shopify Store (`shopify_spend`)

<ResponseField name="shopifyStoreUrl" type="string" required>
  Shopify store domain (top-level field).
</ResponseField>

<ResponseField name="shopifyStoreAccessToken" type="string" required>
  Shopify admin API access token (top-level field).
</ResponseField>

<ResponseField name="metadata.range" type="Array<RewardAmountRange>">
  Range-based rewards by purchase amount.
</ResponseField>

## Token Activity

### Mint an NFT (`MintOn`)

Reward users for minting NFTs.

<ResponseField name="collections" type="Array<Collection>" required>
  Collections to track (top-level field).
</ResponseField>

<ResponseField name="metadata.hasNeverSold" type="boolean">
  Only reward users who have never sold the token.
</ResponseField>

<ResponseField name="metadata.range" type="Array<RewardAmountRange>">
  Optional tiered rewards.
</ResponseField>

### Purchase an NFT (`BoughtOn`)

Reward users for purchasing NFTs.

<ResponseField name="collections" type="Array<Collection>" required>
  Collections to track (top-level field).
</ResponseField>

<ResponseField name="metadata.onlyRewardExistingUser" type="boolean">
  Only reward existing users.
</ResponseField>

<ResponseField name="metadata.hasNeverSold" type="boolean">
  Only reward users who never sold the token.
</ResponseField>

<ResponseField name="metadata.range" type="Array<RewardAmountRange>">
  Optional tiered rewards.
</ResponseField>

<RequestExample>
  ```json theme={null}
  {
    "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
    }
  }
  ```
</RequestExample>

### Hold an NFT (`TokenHold`)

Reward users for holding specific NFTs.

<ResponseField name="collections" type="Array<Collection>" required>
  Collections to track (top-level field).
</ResponseField>

<ResponseField name="metadata.onlyRewardExistingUser" type="boolean">
  Only reward existing users.
</ResponseField>

<ResponseField name="metadata.onlyRewardSingleTokenOwnership" type="boolean">
  Reward only one token ownership per contract.
</ResponseField>

<ResponseField name="metadata.range" type="Array<RewardAmountRange>">
  Tiered rewards by number of tokens held.
</ResponseField>

<RequestExample>
  ```json theme={null}
  {
    "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
    }
  }
  ```
</RequestExample>

### Sell an NFT (`SoldOn`)

Reward users for selling NFTs.

<ResponseField name="metadata.hasSaleCurrency" type="boolean">
  Filter by specific sale currency.
</ResponseField>

<ResponseField name="metadata.saleCurrency" type="string">
  Currency address when hasSaleCurrency is true.
</ResponseField>

<ResponseField name="metadata.range" type="Array<RewardAmountRange>">
  Optional tiered rewards.
</ResponseField>

### Hold a Fungible Token (`token_hold_erc20`)

Reward users for holding ERC20 tokens.

<ResponseField name="metadata.rewardPerValue" type="boolean">
  Reward based on USD value held rather than token count.
</ResponseField>

<ResponseField name="metadata.isTokenHoldMultiplier" type="boolean">
  Apply a multiplier per holding tier instead of fixed points.
</ResponseField>

<ResponseField name="metadata.range" type="Array<RewardAmountRange>">
  Tiered rewards by amount held.
</ResponseField>

### Swap a Token (`swap`)

Reward users for token swaps.

<ResponseField name="metadata.swap.provider" type="string">
  `any`, `relay`, or `lifi`.
</ResponseField>

<ResponseField name="metadata.swap.relayReferrerId" type="string">
  Relay referrer ID when provider is `relay`.
</ResponseField>

<ResponseField name="metadata.swap.from" type="object">
  Source-token filter: `{ mode: "any" | "specific", chain, tokens, trackAmount }`.
</ResponseField>

<ResponseField name="metadata.swap.to" type="object">
  Destination-token filter: `{ mode: "any" | "specific", chain, tokens, trackAmount }`.
</ResponseField>

<ResponseField name="metadata.swap.requireCrossChainSwap" type="boolean">
  Require cross-chain swaps.
</ResponseField>

### Hold tokens in Robinhood (`robinhood_token_hold`)

<ResponseField name="metadata.robinhoodSymbols" type="Array<string>" required>
  Token symbols to track (e.g. `["TRUMP", "SPY"]`).
</ResponseField>

<ResponseField name="metadata.range" type="Array<RewardAmountRange>">
  Optional tiered rewards by holding amount. Falls back to top-level `amount` if
  omitted.
</ResponseField>

## Complex Onchain Logic

### Smart Contract Event (`smart_contract_event`)

Reward based on smart contract events.

<ResponseField name="collectionAddress" type="string" required>
  Contract address to monitor (top-level field).
</ResponseField>

<ResponseField name="metadata.smartContract.event" type="string">
  Event signature to track.
</ResponseField>

<ResponseField name="metadata.smartContract.criteria" type="SmartContractCriteria">
  `everyEvent` or specify conditions.
</ResponseField>

<ResponseField name="metadata.smartContract.params" type="Array<Param>">
  Event parameter filters.
</ResponseField>

<ResponseField name="metadata.smartContract.addressMapping" type="string">
  Address parameter name in event.
</ResponseField>

### Stratus Function (`stratus_function`)

Custom on-chain logic executed by a Stratus function; the function controls reward output.

<ResponseField name="functionId" type="string" required>
  ID of the Stratus function to run (top-level field).
</ResponseField>

<ResponseField name="subscriptionId" type="string">
  Optional Stratus subscription ID (top-level field).
</ResponseField>

<ResponseField name="isDappRule" type="boolean">
  Optionally route rewards to the dApp owner. When true, `rewardType` must be
  `points` and `network` is required.
</ResponseField>

### Provide Liquidity on Uniswap V2 / V3 (`liquidity_uniswap_v2`, `liquidity_uniswap_v3`)

<ResponseField name="metadata.liquidity.network" type="NetworkType" required>
  Chain for liquidity pools.
</ResponseField>

<ResponseField name="metadata.liquidity.protocol" type="string" required>
  Protocol identifier (e.g., "uniswap-v2/eth-usdc").
</ResponseField>

<ResponseField name="metadata.liquidity.pools" type="Array<Pool>">
  Array of liquidity pool addresses (max 20, same network/protocol).
</ResponseField>

<ResponseField name="metadata.liquidity.calculationType" type="LiquidityCalculationType" required>
  `fixed` or `custom`.
</ResponseField>

<ResponseField name="metadata.liquidity.liquidityPerDay" type="number">
  USD value per day (when calculationType is `fixed`).
</ResponseField>

<ResponseField name="metadata.liquidity.customFunction" type="string">
  Custom calculation formula (when calculationType is `custom`).
</ResponseField>

## Developers Contribution

### Star / Fork / Become a Collaborator (`github_repo_star`, `github_repo_fork`, `github_repo_collaborator`)

<ResponseField name="metadata.githubRepoUrl" type="string" required>
  Repository URL. Must match `https://github.com/<owner>/<repo>`.
</ResponseField>

### Merge a GitHub PR (`github_merge_PR`)

<ResponseField name="metadata.githubRepoUrl" type="string" required>
  Repository URL. Must match `https://github.com/<owner>/<repo>`.
</ResponseField>

<ResponseField name="metadata.githubBranchName" type="string">
  Target branch to count merged PRs against. Defaults to `main` if omitted.
</ResponseField>

<ResponseField name="metadata.range" type="Array<RewardAmountRange>">
  Optional tiered rewards by number of merged PRs.
</ResponseField>

## Other

### Submit Text Input (`text_input`)

<ResponseField name="metadata.verifyPlaceHolderText" type="string">
  Placeholder text for the input field.
</ResponseField>

<ResponseField name="metadata.verificationTextMinimumLength" type="number">
  Minimum character length required.
</ResponseField>

### Enter a Code (`code_entry`)

Reward users for entering promo codes.

<ResponseField name="metadata.promoCodeType" type="PromoCodeType" required>
  Type: `code`, `csv`, or `generate`.
</ResponseField>

<ResponseField name="metadata.promoCode" type="string">
  Single shared code (when promoCodeType is `code`).
</ResponseField>

<ResponseField name="metadata.promoCodeCsvUrl" type="string">
  CSV URL with unique codes (when promoCodeType is `csv`).
</ResponseField>

<ResponseField name="metadata.promoCodeLength" type="number">
  Number of codes to generate (when promoCodeType is `generate`).
</ResponseField>

<ResponseField name="metadata.isRestrictedToNewUsers" type="boolean">
  Only allow new users (last 6 hours) to redeem.
</ResponseField>

### Click a Link (`link_click`)

<ResponseField name="metadata.link" type="string" required>
  URL the user clicks.
</ResponseField>

<ResponseField name="metadata.timeDelayToVerifySeconds" type="number">
  Delay (1–600 s) before the reward is granted.
</ResponseField>

<ResponseField name="metadata.socialPlatform" type="SocialPlatform">
  Platform shown for the link (e.g. X, Discord, or `Custom`).
</ResponseField>

<ResponseField name="metadata.buttonText" type="string">
  Label on the action button.
</ResponseField>

### Answer a Quiz (`quiz`)

<ResponseField name="question.text" type="string" required>
  Question text.
</ResponseField>

<ResponseField name="question.choices" type="Array<Choice>">
  Answer options with `isCorrect` flag. Exactly one choice must be correct.
</ResponseField>

### Answer a Poll (`poll`)

<ResponseField name="question.text" type="string" required>
  Poll question text.
</ResponseField>

<ResponseField name="question.choices" type="Array<Choice>">
  Answer choices.
</ResponseField>

<ResponseField name="question.allowMultipleAttempts" type="boolean">
  Allow users to vote multiple times.
</ResponseField>

### Participate in Governance Vote (`snapshot_governance`)

<ResponseField name="metadata.snapshotProposals" type="Array<{ id: string, space: string }>" required>
  Snapshot proposal(s) to track. One proposal per rule.
</ResponseField>

### Create a Partner Account (`create_partner_account`)

<ResponseField name="metadata.link" type="string" required>
  Partner site URL. Must match the partner auth domain.
</ResponseField>

<ResponseField name="metadata.buttonText" type="string">
  Label on the action button.
</ResponseField>

### External Rule (`external_rule`)

Custom rules managed by external services.

<ResponseField name="amount" type="number" required>
  Base points amount.
</ResponseField>

<ResponseField name="trackProgress" type="boolean">
  Track completion progress (top-level field).
</ResponseField>

<ResponseField name="metadata.range" type="Array<RewardAmountRange>">
  Range-based rewards with custom amounts.
</ResponseField>

<ResponseField name="metadata.rewardPerAction" type="boolean">
  Multiply reward by number of actions.
</ResponseField>

<ResponseField name="metadata.conditionDescription" type="string">
  Description of what the external rule tracks.
</ResponseField>

### Points Airdrop (`points_airdrop`)

Airdrop points to specific users via CSV.

<ResponseField name="loyaltyUserAllotmentCsvUrl" type="string" required>
  CSV URL with wallet addresses and point amounts (top-level field).
</ResponseField>

<ResponseField name="trackProgress" type="boolean">
  Track processing progress.
</ResponseField>

## 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`.

<ResponseField name="metadata.range" type="Array<RewardAmountRange>" required>
  Tiered rewards by rank/threshold for the chosen metric.
</ResponseField>

<ResponseField name="dappDataWindow" type="LoyaltyRuleInterval" required>
  Data window for the metric: `daily`, `weekly`, or `monthly`. Leave empty for
  all-time.
</ResponseField>

<ResponseField name="dappDeployedWithin" type="LoyaltyRuleInterval">
  Optional filter limiting to dApps deployed within this window.
</ResponseField>

<ResponseField name="isDappRule" type="boolean">
  Routes rewards to the dApp owner. Immutable after creation.
</ResponseField>

<ResponseField name="network" type="NetworkType" required>
  Blockchain network for the rule (required when `isDappRule` is true).
</ResponseField>

<Info>
  `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.
</Info>

## Range-Based Rewards

Some rules support range-based rewards with tiered amounts:

<ResponseField name="metadata.range" type="Array<RewardAmountRange>">
  Array of reward ranges.
</ResponseField>

<ResponseField name="startRange" type="number" required>
  Minimum value for this range.
</ResponseField>

<ResponseField name="endRange" type="number" required>
  Maximum value (use `Number.MAX_SAFE_INTEGER` for unlimited).
</ResponseField>

<ResponseField name="amount" type="number" required>
  Points awarded for this range.
</ResponseField>

<ResponseField name="loyaltyBadgeId" type="string">
  Optional badge for this range.
</ResponseField>

### Range Examples

<AccordionGroup>
  <Accordion title="NFT Token Hold">
    ```json theme={null}
    "metadata": {
      "range": [
        {
          "startRange": 1,
          "endRange": 10,
          "amount": 100
        },
        {
          "startRange": 11,
          "endRange": 50,
          "amount": 500
        },
        {
          "startRange": 51,
          "endRange": 9223372036854775807,
          "amount": 1000
        }
      ]
    }
    ```
  </Accordion>

  <Accordion title="ERC20 Token Hold (Multiplier)">
    ```json theme={null}
    "metadata": {
      "range": [
        {
          "startRange": 1,
          "endRange": 9223372036854775807,
          "amount": 1.5
        }
      ],
      "isTokenHoldMultiplier": true
    }
    ```
  </Accordion>
</AccordionGroup>

## Advanced Configuration

### Max Reward Limits

<ResponseField name="maxAmountPerInterval" type="number">
  Maximum points users can earn in the interval.
</ResponseField>

<ResponseField name="maxAmountInterval" type="LoyaltyRuleMaxAmountInterval">
  Interval period: `daily`, `weekly`, `monthly`.
</ResponseField>

### Claim Type

<ResponseField name="claimType" type="LoyaltyRuleClaimType">
  `manual` (user must claim) or `auto` (auto-rewarded).
</ResponseField>

### Reward Lifetime

<ResponseField name="rewardLifetime" type="LoyaltyRuleRewardLifetime">
  For multipliers/badges: `permanent` (never removed) or `dynamic` (removed if
  requirements not met).
</ResponseField>

### Airdrop Duration

<ResponseField name="effectiveStartTime" type="ISO8601">
  When airdrop becomes effective.
</ResponseField>

<ResponseField name="effectiveEndTime" type="ISO8601">
  When airdrop ends.
</ResponseField>

## Validation Examples

<AccordionGroup>
  <Accordion title="Basic Points Rule">
    ```json theme={null}
    {
      "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": {}
    }
    ```
  </Accordion>

  <Accordion title="NFT Hold with Custom Rewards">
    ```json theme={null}
    {
      "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"
      }
    }
    ```
  </Accordion>

  <Accordion title="Social Quest with Range Rewards">
    ```json theme={null}
    {
      "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
          }
        ]
      }
    }
    ```
  </Accordion>
</AccordionGroup>

## Error Handling

<Warning>
  All required fields must be provided. Missing required fields will result in a
  400 Bad Request error with details about which fields are missing.
</Warning>

<Warning>
  Range values must not overlap. The start range should be less than or equal to
  the end range for each range entry.
</Warning>

<Info>
  For Ultra networks (Ultra/UltraTestnet), the collection address format is
  `address::symbol`. For example: `accountname::TOKEN`.
</Info>

<Tip>
  Use the loyaltyCurrencyId field consistently across rules to ensure proper
  currency management in your loyalty program.
</Tip>

## Rule Type Limitations and Restrictions

### Multiplier Reward Type Support

<Warning>
  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.
</Warning>

### Claim Type Support

Different rule types support different claim types (manual vs auto-reward):

<Tabs>
  <Tab title="Manual Only">
    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`
  </Tab>

  <Tab title="Auto Only">
    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`
  </Tab>

  <Tab title="Both Manual & Auto">
    These rules support both claim types:

    * `external_rule`
    * `telegram_messages`
    * `connect_wallet`
    * `connected_epic`
  </Tab>
</Tabs>

### Reward Lifetime Options

The `rewardLifetime` field (for multipliers and badges) has different support levels:

<AccordionGroup>
  <Accordion title="Permanent Only">
    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

    <Info>
      With `permanent` lifetime, once a user qualifies for the reward, they keep it forever even if they stop meeting requirements.
    </Info>
  </Accordion>

  <Accordion title="Dynamic & Permanent">
    These rules support both `permanent` and `dynamic` lifetime:

    * `TokenHold` - Most flexible, supports both
    * `external_rule` - Can expire if user stops meeting requirements

    <Info>
      With `dynamic` lifetime, rewards are removed if the user no longer meets the requirements (e.g., sold their NFT).
    </Info>
  </Accordion>
</AccordionGroup>

### Interval Restrictions

Some rules have limited interval options:

<Info>
  Rules with `frequency: immediately` typically only support `interval: once` or specific intervals.

  Rules with `frequency: none` may support custom intervals.
</Info>

### 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`

<Warning>
  Range-based rewards require the `metadata.range` array. Maximum range value is
  `Number.MAX_SAFE_INTEGER` (use this for unlimited).
</Warning>

### Network-Specific Limitations

<Info>
  For **Ultra** and **UltraTestnet** networks, collection addresses require a
  symbol suffix: `address::symbol` (e.g., `accountname::TOKEN`)
</Info>

### Token Holdings

<Warning>
  ERC20 token holding rules (`token_hold_erc20`) require network support for USD
  price tracking. Networks without USD support will disable the `rewardPerValue`
  option.
</Warning>

### Liquidity Pool Limits

<Info>
  Liquidity rules support a maximum of **20 pools** per rule, all within the
  same network and protocol with the same factory address.
</Info>

### Shopify Integration

<Warning>
  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.
</Warning>
