# Introduction

Spindl is a measurement and attribution platform for Web 3. \
\
What that means is we can tell you where your users came from (Twitter? Telegram? An on-chain airdrop?) and what they did on-chain while inside your dApp. In other words, did they generate revenue? Did they stick around? Did they cost too much to acquire?

(If you're a Web 2 marketer who got crypto-pilled like us, think of us like a Web 3 Branch or Adjust or AppsFlyer, with some added Web 3 magic.)

Visually, the unique views you get inside Spindl look as so: Web 2 channels like Twitter appearing, correctly attributed!, next to Web 3 ones like quests and on-chain rewards. <br>

<figure><img src="/files/vYjlsSkBro632d8RLscw" alt=""><figcaption></figcaption></figure>

If the wonky-sounding term 'attribution' doesn't mean anything to you, click on the link at bottom to learn more. \
\
If you just want the quick-start guide:

{% content-ref url="/pages/YoRqDa29v11YJhPfyxYG" %}
[Start Here](/spindl/techncial/start-here)
{% endcontent-ref %}


# Attribution

Making the crypto cash register ring

Attribution system is the capital 'T' Truth of a media ecosystem, establishing what upstream publisher or action--a click on a Tweet, an Instagram ad--led to some valuable downstream user action like an install or a purchase.&#x20;

In its pure and perfect form, attribution lets you measure along what's known as the marketing 'funnel': the narrowing cohort of users who progressively advance from Discord post to click to wallet sign-in to some on-chain (or off-chain) user action. If you know how you monetized 'down funnel' inside your app experience, and you know how much you spent getting the users there (either via paid media like ads, or even organic media like blog posts), then you can calculate your 'ROAS' (return on advertising spend....marketing tech has lots of acronyms). Pristinely, it looks somewhat like this:

<figure><img src="/files/M3RJDQgQsCxfA2BRJxaE" alt=""><figcaption></figcaption></figure>

If ROI > 1, you have a viable business. If it isn't....you don't. \
\
This is the unforgiving math of any business, digital or analog. The difference is in the digital world you can (almost) measure all of it out to four decimals.


# Privacy

Our human-readable explainer on our approach to privacy is here:

{% embed url="<https://blog.spindl.xyz/p/the-spindl-take-on-web-3-privacy>" %}

Data Processor Addendum and formal data use docs available by request.


# Your Spindl app setup

How to get API keys, add users, and more

You can manage most aspects for your Spindl app by hitting the gear ⚙️ emoji on the upper right-hand side.&#x20;

<figure><img src="/files/qpqdWh0ATfO7SfEC1xYd" alt=""><figcaption></figcaption></figure>


# Managing team members

The members window allows you to see who has what level of acess, and invite new team members.&#x20;

If you want to add someone from a new email domain, shoot us a note to add that domain (we don't allow rando emails to be added).&#x20;

<figure><img src="/files/GSDALKoAPShm6VyckcSt" alt=""><figcaption></figcaption></figure>


# Onchain Attribution

Doing real attribution in Web 3 presents unique challenges. Bluntly, most people in the crypto space who claim to do so, don't (at least not by most definitions of the somewhat squishy term). \
\
Briefly, attribution answers the question:&#x20;

*Where did this user really come from?*&#x20;

By extension, it also answers like:&#x20;

* *How long did they stick around?*&#x20;
* *How much do I owe the source of the user for the acquisition?*&#x20;
* *How much revenue did I make off this user and am I happy with the acquisition channel?*<br>

The question is hard because it requires keeping stateful information about a user, and even defining what an active user is (i.e. an active user is one that transacts at least once in 7 days).&#x20;

It's not as simple as merely noting what website they were on before they got on your app (via UTM parameterss say), or even noting that this wallet address that transacted via your protocol was also touched by some other upstream smart-contract (the user touched lots of smart contracts). *Which is the one we should credit with the new user?* is the question real attribution answers.&#x20;

This diagram describes one such situation:

<figure><img src="/files/OXVzSBMVoZVptpjnJ4Fq" alt=""><figcaption><p>The tangled tale of a user acquisition journey</p></figcaption></figure>

Spindl, uniquely among any and all attribution providers, merges offchain and onchain events intelligently to seamlessly answer these questions. Using our constantly-updated identity, we can interlace Web2 and Web3 funnel arbitrarily, hiding the gritty deals from the user.&#x20;

As an example, here's&#x20;


# Plotting attribution

You can do most everything inside our trusty Chartbuilder (accessible almost everywhere as a tab up top).&#x20;

The key thing to keep in mind is that you can only do attribution on bottom-of-funnel events that have been tagged as 'conversion events,' which are typically (though not always) onchain events like mints or swap.&#x20;

While Spindl makes cross-Web2/Web3 attribution both possible and easy, there's only so much we can abstract away.&#x20;


# Web3-native Analytics

Imagine Web2 analytics platforms like Amplitude and Mixpanel rebuilt with native support for Web3. \
\
Spindl analytics is just about that.&#x20;

Wallet connect rates on your landing page, retention rates and ROIs on your Telegram or Discord traffic, flow charts from offchain to onchain, and more. Stop hacking Dune queries and mashing them with Amplitude dashboards, and starting using real tooling.

<br>


# Chart Builder

The ChartBuilder tool is similar to similar tools inside MixPanel or Amplitude, but with native support for onchain events (and offchain ones). \
\
Click the ever-present ChartBuilder up top and choose among chart types:

* Line Chart
* Funnel
* Cohort Analysis
* Data Table
* 'Big Numbers'
* Sankey diagrams
* Pie/Donut charts

<figure><img src="/files/qOkalcfg2cVeTNKxQvtE" alt=""><figcaption></figcaption></figure>


# Event Selection and Filtering

ChartBuilder lets you filter and plot every event you ingest by every piece of metadata available (including onchain data). Segmenting and filtering those events can be tricky however. \
\
Open up 'ChartBuilder' on the main Spindl page and choose line chart from the menu. Pick a few events on the left-hand side (here we've picked page view, wallet connect, and an onchain swap). \
\
The plot below is essentially the offchain to onchain converson funnel for this sample client (we'll plot it in a funnel view in below).&#x20;

<figure><img src="/files/5ysCKDGHXkGf0bxbooKC" alt=""><figcaption></figcaption></figure>

In reality, Spindl is just a very sophisticated event ingestion engine, surfacing those events with visualizations and various bits of computation. This menu of events on the left will re-appear in every chart type, and also in [Audiences](/spindl/features/audiences). Understanding how to view your users through the events they trigger is a key part of using Spindl. \
\
A couple pointers on the event picker and filtering pane on the right. \
\
Aggregates: Depending on the event, the 'measured as' could be literal counts of the event firing, unique users, unique sessions, revenue...or much else besides depending on the event. **Make sure you're measuring the number you think you are, and that events plotted together have comparable measures.**

<div align="left" data-full-width="false"><figure><img src="/files/tWWS6IFhqSAXryUswUuI" alt="" width="306"><figcaption><p>The aggregate picker for Page View.</p></figcaption></figure></div>

The next control is the filter toggle, which can be used to drill down into various pieces of event metadata.&#x20;

\
![](/files/I5HVKKlnr7HNuzDsDDkk)

Just about about every piece of event metadata, whether offchain or onchain, is available here.  If it's not, ask us.&#x20;

**Note that onchain events have very different metadata,** and will not have your usual Web2 state like browser or URM parameters.&#x20;

<div align="left"><figure><img src="/files/QB0idPE1xP9GtdLHDz0T" alt="" width="375"><figcaption><p>The onchain data filter for swapping transactions.</p></figcaption></figure></div>

Lastly, there's the segment tool, which aggregates by the event filter chosen, and plots data as a table underneath (almost) any chart type you choose.&#x20;


# Line Chart

## Line chart

The bread and butter of analytics, behold!, the venerable line chart.&#x20;

This is a great place to get started with Spindl charting, as the left-hand event navigation is (roughly) consistent across chart types, and is pretty simple in this case. \
\
Here are trades pivoted by attributed channel (for how to pivot and segment, see here):\ <br>

<figure><img src="/files/GQeCmVIVpXT2Jyi1Hi6p" alt=""><figcaption></figcaption></figure>


# Funnel

The central metaphor for all marketing is the funnel: the narrowing cohorts of users as they pass from initial action to eventual sale.&#x20;

Spindl lets you easily plot arbitrary events by filtering and ordering them on the left-hand picker, as you do almost everything in [ChartBuilder](https://docs.spindl.xyz/spindl/features/web3-native-analytics/chartbuilder).&#x20;

Careful! The funnel is necessarily a decaying number of events or users: counts in the rightmost buckets *only* appear if a user passed through the events to the left. Meaning, the total in *token received* below is only the tokens received by people who started the funnel in 'page views from Twitter users' on the left.&#x20;

<figure><img src="/files/Iwl7av9ucSGHWrgRMGY9" alt=""><figcaption><p>A typical funny from two page views to a wallet connect to an onchain action.</p></figcaption></figure>


# Cohort Retention

## Cohort Retention

One of the classic (and harder to interpret) data visualizations of growth marketers is the cohort retention graph. Such a plot shows you the retention of a given cohort through time (reading left to right on the below chart), or of the same time period (e.g. two time periods in) across different starting cohorts (reading from top to bottom). \ <br>

<figure><img src="/files/5SJ267kllYns6LC2XFFF" alt=""><figcaption><p>Something really compelling for new users happened around Jan. 22nd, and Feb. 26th. Also, Feb. 12th cohort got re-engaged by whatever happened on Jan. 29th. </p></figcaption></figure>

As befits a complex plot, the configuration is also slightly complex. The Activating Event is the event that definitionally marks the beginning of the user journey for the purposes of the plot (here, having done a swap). The Retaining Event is what class of event the user must have done to be considered an active user in that cohort (here, the relatively light-touch event of a page view). The period is simply the size of the time step used to define the cohort: daily, weekly, or monthly.&#x20;

<figure><img src="/files/5r6qbKaBUv7Nmb69vQWw" alt="" width="375"><figcaption></figcaption></figure>

If the retention plot looks too small (only 3x3 say), either reduce the period to something shorter, or lengthen the date range in the upper right-hand side date picker.&#x20;


# Sankey Diagram

Arguably an even more complex chart type is [the Sankey diagram](https://en.wikipedia.org/wiki/Sankey_diagram), named after an Irish rail engineer plotting flows in a steam engine (though preceded by [the much more famous Minard map](https://bigthink.com/strange-maps/229-vital-statistics-of-a-deadly-campaign-the-minard-map/), which demonstrated the horrifying casualties suffered by Napoleon's army as it marched to and from Moscow). \
\
This one is a bit of a brain teaser, and the Spindl version is a pretty basic rendering of the full power of a Sankey, but it can be helpful to visualize certain flows.&#x20;

Here's a Sankey showing the Page View entry points for a dex, filtered to traffic from Twitter (pretty high wallet connect rate!):

<figure><img src="/files/p4zVnZpI2x1ho8enuAAT" alt=""><figcaption></figcaption></figure>

If you want to see what swaps get traded before users drop off, here's one view:<br>

<figure><img src="/files/XugfaiRp3WPdbpzAyqVK" alt=""><figcaption></figcaption></figure>

The configuration pane can be a bit confusing, and the output almost random-seeming (a lot of it depends on layout parameters, like depth of journey, that we don't expose yet).

Some definitions:

* Event - The primary event to plot and focus on, though if users hit other events, those will appear too. If you filter, the entire chart will only be limited to events of that filtered type.
* Direction - 'Entering event' means that generally you're looking at a plot of many origin points going to one final endpoint. 'Leaving event' means you're looking at the focus event as the source of the flow, and seeing where users go after that. If you reverse it in your head a lot, join the club; so do we.&#x20;
* Expanded Events - Here you can force events to expand into different sources (or sinks) based on whatever filter dimension you choose (e.g. session channel, AKA, where they came from).&#x20;

It's hard to get a hang of it at first; play around the params until it sort of makes sense.&#x20;


# Big Numbers

This 'chart type' is really just a headline big number for the top of dashboards. Pick your event, pick your aggregate measure, filter if you want....and then save to the dashboard as your big new KPI.&#x20;

<figure><img src="/files/oappPkEYf5wFTpHlM7LV" alt=""><figcaption><p>Good headline number for the top of a dashboard</p></figcaption></figure>

##


# Pies and Donuts

PSA: The makers of Spindl fairly despise pie charts, but sometimes they're the most efficient data visualization and we can't call ourselves an analytics platform without something like it.&#x20;

<figure><img src="/files/I64AlRgrS3codA60nCSP" alt=""><figcaption><p>Trades by attribution channel.</p></figcaption></figure>

The event configuration on the right is like most charts in Spindl (we keep it consistent for a reason).&#x20;


# Audiences

Audiences, segments, cohorts, buckets: call them what you will, but groups of users defined by your own events (or outside third-party events) are a critical tool for both measurement and (hopefully) targerting.

Here's a very typical overview of a protocol's entire active users, with basic stats like size, active wallets (defined as MTWs), transactions, median balance, etc. \
\
Note also the donut charts for transactions by chain, token balances and (importantly) the engagement distribution of users (**including a Sybil model).**&#x20;

<figure><img src="/files/EAkLCG8wWCeqOgjBFcWP" alt=""><figcaption></figcaption></figure>

But first, how to create an audience (other than your default one of every user).&#x20;


# Creating an audience

After hitting 'Create Audiences' on the upper right-hand side, you'll see an event picker menu somewhat like that of [ChartBuilder](/spindl/features/web3-native-analytics/event-selection-and-filtering). \
\
The difference though is you can arrange them in a circuit of ANDs and ORs. As with ChartBuilder, you can filter on specific event metadata, either offchain (like session channel here) or onchain data (like swaps pair). \
\
Also give the audience a name, as this Audience will be accessible from multiple places inside the app.&#x20;

<figure><img src="/files/wVGkWLV4Wvt4twJAPAQN" alt=""><figcaption></figcaption></figure>


# Short Links

Short Links allow you to easily create custom links for each campaign, source, or post.

By default, analytics that live on your website will pick up parameters like the Page Referrer and UTM Source, but there are many sources which are "invisible" (ie, if you click a link from Discord's native application, it will appear as "Organic") or not very granular (while you can tell traffic is from Twitter, it's impossible to see which tweet or biolink generated it). Spindl links let you create links at any granularity - per campaign, per source, or per post - to let you capture the analytics that matter most.

{% embed url="<https://twitter.com/0xPolygonLabs/status/1595484025799708673>" %}
An example Spindl short link
{% endembed %}

## Creating Links

Creating a short link on Spindl is as simple as providing a name and destination URL. From there, we'll generate a short link that you can share publicly. From there, you can track the engagement and downstream conversions.&#x20;

<figure><img src="/files/ADpAhpzdINr2C382whDi" alt=""><figcaption></figcaption></figure>

## Custom Domains

In addition to short links on the spindl.link domain, Spindl supports custom domains as well (so you can have links like `share.mydapp.xyz/abcd`). Custom domains require a little more set up, but can often better represent your brand and increase trust with customers.

To get started with Custom Domains, please out to the Spindl team.


# Custom Domains

By default, Spindl's short links look like: <https://spindl.link/b2b3ec7a5>

Spindl supports custom domains as well, so you can have links like <https://link.mydapp.com/12345>.&#x20;

### DNS Changes

To set up a Custom Domain, you need to make two simple changes to your DNS settings:

1. Create a CNAME record pointing the custom subdomain to Spindl's redirect server (`custom-domains.spindl.click.)`
2. Create a TXT record for SSL certificate validation & creation (this is specific to each subdomain)

| Record Type | Name                  | Value                            |
| ----------- | --------------------- | -------------------------------- |
| **CNAME**   | link                  | `custom-domains.spindl.click.`   |
| **TXT**     | \_acme-challenge.link | (We'll send it to you privately) |

As an example, here is what this should look like in different providers:

* Cloudflare (for the subdomain `l`)

<figure><img src="/files/N069BccnK8aCZugMKVYF" alt=""><figcaption></figcaption></figure>

* Namecheap (for the subdomain `link`)

<figure><img src="/files/VXY2Q98dtVOuhkzm789D" alt=""><figcaption></figcaption></figure>

After configuring records, it can take up to a few hours for the changes to propagate and the SSL certificate to be generated.


# Referrals


# Management and reporting

A referral system is a natural add-on to a proper attribution system, as it's effectively adding a business model to the question of "who brought in this user?"\
\
That referrer can be anyone: an influencer, a popular community member, or even a publisher like CoinGecko (the biggest referrer on the GMX referral system). The attribution is effectively agnostic: every referrer, whether KOL or *The New York Times*, is treated equality in the attribution system as yet another channel. \
\
You can pay out the referrer as a bounty (i.e. CPA), or what we call [CPV](https://blog.spindl.xyz/p/cost-per-value-cpv-a-new-media-business) (a revenue share on user value). You can do tiers based on volume (e.g. bounty A if the referred user trades up to X amount, and bounty B if it's more). You can do practically anything: if it's onchain (or even offchain), Spindl can measure it and build a referral model around it. <br>

<figure><img src="/files/UWmFd67pwy8oV00tVWUj" alt=""><figcaption></figcaption></figure>


# Quick Start

Getting started with a new Referral Program is as simple as:

1. Install the [Javascript SDK](/spindl/techncial/javascript-sdk-html-script-guide): Installing the SDK automatically instruments Page Views and Wallet Connection events, and will connect a referral link to an on-chain actions. The SDK Is lightweight and can be configured in a few lines of code.
2. Allow users to create [Referral Codes](/spindl/techncial/javascript-sdk-html-script-guide/referrals): The SDK provides functions to easily create custom referral codes directly from your frontend, to make it easy to let all users act as referrers.
3. Allow users to [withdraw their rewards](/spindl/techncial/on-chain/rewards): Ultimately, we want to show users how much they earned and let them easily withdraw it. Spindl creates a smart contract for you under the hood, and provides easy to use functions to interact with it.


# Technical Details

## Overall Flow

1. Campaign Managers create an on-chain campaign, and deposit `ETH` or `ERC20s` towards the `totalBudget` of the campaign. This is currently accessible through Spindl's Web App (where a trusted worker can do certain administrative actions on behalf of the campaign manager)
2. Relevant Recipients (or Publishers) are added to each campaign (using `addRecipients`). They generate traffic to fulfill the campaign's criteria. For example a recipient can be an influencer that drives traffic for certain purchases such as an NFT
3. Recipients can also be implicitly created via `updateBalances` which is explained in detail further down
4. The Spindl server updates the latest balances for the given campaign and its recipients. Spindl will do this on a recurring, periodic basis.
5. Recipients can then withdraw their earned funds

## Technical Details

### Architecture Design

This Architecture follows the [Beacon proxy](https://docs.openzeppelin.com/contracts/4.x/api/proxy#beacon) & Factory patterns.

The specification contains 4 types of contracts:

1. Many **Proxy Contracts (One Per Campaign Manager)**
2. One **Beacon Contract**
3. One **Implementation Contract**
4. One **Factory Contract** that creates proxy contracts

A cron job periodically triggers the proxy contracts to update the balances for each campaign manager.

<figure><img src="/files/OAs4JZ7l1F5qdJqqKkbE" alt=""><figcaption></figcaption></figure>

**Terminology**:

*Owner* - administrative Multi-Sig wallet that is the owner of important contracts such as Factory and Proxy and has owner privileges in case of issues.

*Worker* - an address that will be assigned to server to be able to make earnings updates on behalf of Spindl or the end user to the smart contract. Access can be revoked by the owner.

*Manager* - An advertiser running campaigns, who is responsible for funding campaigns.

*Recipient* - in most cases this will be the referrer or referent for the referral campaign. In the context of the smart contract, they are recipients of earnings.

*Campaign* - Similar to other marketing platforms, we want to distinguish different campaigns by the end goals. Multiple campaigns can live in the same smart contract.

#### Backend Flow

This is the typical scenario we should be following:

1. When a new campaign manager (Advertiser) is onboarded, Factory creates a new proxy campaign manager contract using `deployProxy`.
2. When campaign manager is ready, a campaign is created using `createCampaign` and recipients are added using `addRecipients`
3. Campaign Manager adds funds to campaign using `fundCampaign` to the campaign. Campaign Manager can continue to add more funds later while campaign is ACTIVE.
4. Campaign is set to status ACTIVE by server.
5. When server calculates attributions, it can update earnings balances for campaigns using `updateBalances`. We can also implicitly create recipient records with this method if a recipient does not exist
6. Once a recipient has new earnings, we can check their balance using `getRecipient` to see the amount. They can then withdraw those using `withdrawRecipientEarnings`. If a user has pending balances from multiple campaigns, they can use `withdrawManyRecipientEarnings` and pass in the campaigns.
7. Once campaign is complete, Campaign is set to status of COMPLETED. The Campaign Manager cannot add funds to campaign at this point.
   * If the entire budget of campaign is not spent, then campaign can be set to status COMPLETED by the server. The Campaign Manager will then be able to withdraw (using `withdrawRemainingCampaignBalance`) the difference between totalBalance and totalEarned so we only want to update status to COMPLETED when attribution of earnings is complete.
   * Campaign can be re-started (i.e changed back from COMPLETED to ACTIVE) & be funded by campaign manager again. The reason is that the general desire in this contract is to keep features flexible.

#### Proxy Contract

This contract shall act as a proxy for the Implementation Contract. It will request the address of the Implementation from Beacon Contract. Once retrieved, it will forward function calls and transactions to the Implementation Contract using `delegateCall`, while keeping track of state within itself per campaign manager. Also as a result of this, the transaction will use the state of storage variables present in the Proxy Contract, not the Implementation Contract.

#### Beacon Contract

Refer to OZ's documentation for the [Beacon pattern](https://docs.openzeppelin.com/contracts/4.x/api/proxy#beacon). This contract holds reference to which implementation contract the proxies should point to. The reason for this approach is that when we want to update 100 proxy contracts to v2 of implementation, we have to only update the beacon contract to point to the v2 implementation & all the proxies get this reference from beacon. The alternative approach we considered was a standard UUPS approach, but in that case we would have to update all the 100 proxies to point to the v2 implementation.

#### Implementation Contract

The Implementation Contract is where the bulk of the business logic resides. It is the contract that will be upgraded, and it is the contract that will be called by the proxy contract. The Implementation Contract will be deployed once and then upgraded as needed.

It is responsible for having methods that proxy needs to keep track of and update state

**Every future Implementation Contract must have the methods to upgrade itself since we are using Upgradeable Beacon pattern.** Open Zeppelin upgradable packages should take care of this by default, but please add tests as needed.

#### Factory Contract

* Will have ability to deploy new Proxy contract per Campaign Manager
* It is useful to have a centralized contract that deploys so we can index the arbitrary proxies that are created

### UX for end-users from Front-end

#### Advertiser UI Flow

* Campaign Manager logins into Spindl App and creates Campaign, specifying the "conversion" events and associated payout information
* Spindl's backend creates a new Proxy Contract on-chain, if one that doesn't already exist
* Spindl's backend creates a new campaign (`createCampaign`) on-chain with the specified currency (ETH or pre-approved ERC20's)
* The Campaign Manager funds the campaign
* Spindl updates the balances accordingly

#### Recipient UI Flow

* Gets a link from campaign manager if they are invited to be part of referral campaign
* Starts advertisting the product & wait for Spindl do add attribution
* Can log in with their wallet to recipient dashboard, check their balances & withdraw. (they will pay gas for this)

### FAQs

**How to remove a recipient?**

You cannot remove the mapping of an existing refer. An owner or worker can set the recipient.status = PAUSED with `setRecipientStatus` & they will not be able to withdraw money

**How to refund an campaign manager if they didn't spend their campaign funds?**

At the moment this has to be done manually by owner or front-end using a few steps.

1. Campaign should be set to status `COMPLETED` so that people cannot add recipients, etc. The attribution of all earnings should be done at this point.
2. Campaign Manager can withdraw the difference between `totalEarned` and `totalBalance` using `withdrawRemainingCampaignBalance`

**What validation is in place to ensure balances are correct for recipients & campaigns?**

1. You cannot over-attribute a campaign. For example, in campaign ABC an campaign manager added a totalBalance of $100 to campaign & 2 recipients are added. First campaign manager earned $60 from ABC, then the contract doesn't allow the earned amount of 2nd campaign manager to be more than $40. This prevent over attribution on campaign level.
2. Also recipient cannot withdraw more than they earned. This prevents recipients withdrawing more than the totalBalance of the campaign since #1 is in place.
3. If the campaign becomes stale, and recipients don't earn entire balance of campaign, then campaign can be completed & campaign manager can withdraw the difference between totalBalance & totalEarned in campaign. The totalBalance is then updated to reflect this, & campaign manager cannot withdraw again

**How can the owner pause the proxy contract?**

1. Owner can set worker address to address(0) to prevent server from making any updates
2. They then can pause all the campaigns within the campaign manager proxy. This will prevent recipients from withdrawing money or from any balances being updated although campaign manager will still technically be able to add more funds to the campaign

**If recipients' address get compromised, during an active campaign, how do we resolve this?**

If a recipient loses their private key they will be unable to withdraw any earnings from the contract. The recipient will need to contact the owner to resolve the issue. They will need to first create a new referral account using a different wallet address and have the old account paused. Then they will need to contact the owner to transfer the remaining earnings from the old recipient to the new recipient. The owner will need to verify that the new recipient is the same person as the old recipient.

> Note there are no plans to implement a way to transfer earnings from one recipient to another in the contract. This is because of the additional security risks that would be introduced by allowing this. Instead, the owner will need to modify the state of the ledger on the backend servers and wait for another `updateBalances` to be called to update the state of the contract.

**How do recipients withdraw their earnings?**

* They should use `withdrawRecipientEarnings` or `withdrawManyRecipientEarnings`
* Recipient should be in status `ACTIVE`
* If Campaign status is `PAUSED`, then recipient cannot withdraw
* cannot individually withdraw more than difference between earned & already withdrawn
* cannot individually withdraw more than total budget of campaign.

**How is the server wallet `worker` kept secure**

We are using MPC wallet technology to prevent and limit surface are of attack.

**What are the different Campaign States & what do they do?**

| State     | Description                                                       | <p>Can balances<br>be updated?</p> | <p>Can recipient<br>be added?</p> | <p>Can Campaign<br>be funded?</p> | <p>Can recipient<br>withdraw?</p> | <p>Can campaign state<br>be updated in<br>current state?</p>          | <p>Can campaign manager<br>withdraw unspent campaign<br>balance?</p> |
| --------- | ----------------------------------------------------------------- | ---------------------------------- | --------------------------------- | --------------------------------- | --------------------------------- | --------------------------------------------------------------------- | -------------------------------------------------------------------- |
| NONE      | <p>null state. campaign hasn't<br>been created yet.</p>           | NO                                 | NO                                | NO                                | NO                                | <p><strong>YES</strong> (only via<br><code>createCampaign</code>)</p> | NO                                                                   |
| CREATED   | <p>campaign is created<br>during <code>createCampaign</code>.</p> | NO                                 | **YES**                           | **YES**                           | NO                                | **YES**                                                               | NO                                                                   |
| ACTIVE    | <p>campaign can be set active via<br>setCampaignState</p>         | **YES**                            | **YES**                           | **YES**                           | **YES**                           | **YES**                                                               | NO                                                                   |
| PAUSED    | <p>campaign is paused to prevent<br>withdrawals and funding</p>   | **YES**                            | NO                                | NO                                | NO                                | **YES**                                                               | NO                                                                   |
| COMPLETED | <p>campaign is successfully<br>completed.</p>                     | **YES**                            | NO                                | NO                                | **YES**                           | **YES**                                                               | **YES**                                                              |

**What are the different Recipient States & what do they do?**

| State  | Description                                                                         | Can recipient withdraw?                                                   |
| ------ | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| NONE   | <p>null state.<br>recipient is not created yet</p>                                  | NO                                                                        |
| ACTIVE | default state after recipient is created                                            | <p>Only if Campaign is <code>ACTIVE</code>,<br><code>COMPLETED</code></p> |
| PAUSED | <p>paused to prevent recipient from withdrawing &<br>to potentially sunset them</p> | NO                                                                        |

<br>

\ <br>


# Start Here

To understand the attribution process, we need to understand the event types we can collect and then implement the collection process via GTM, SDK, or API.

## Event Types

#### **Page View**

When a user navigates through pages, we track basic information on which page the user is on as well as where they are coming from (which websites/apps/users are referring the user to your app or site)

#### **Wallet Connect**

When a user connects their wallet address, we prefer to collect this information to understand their public preferences (e.g., are they a DeFi user, NFT enthusiast, etc.).

**Custom**

There are many important custom events on any website or app that we want to track (form submission, button clicks, sign-ins, etc.) as these can be important conversion events in the funnel. Custom event collection allows us to track these.

## Implementation

There are 3 ways to implement the Spindl SDK and start sending events to us depending on your type of application & existing integrations.

{% tabs %}
{% tab title="Google GTM" %}
We recommend this for most websites that have Google's [GTM](https://marketingplatform.google.com/about/tag-manager/) already installed. You can achieve a lot without ever changing the code in your webiste.

For GTM Integration, go [here](/spindl/techncial/google-gtm-guide).
{% endtab %}

{% tab title="Web/Mobile SDK" %}
Here, we can use either the NPM library, Web Script, or Android/iOS SDK to implement SDK into your applications. We recommend this if you have a mobile app or you have a web app that doesn't have Google GTM.

Follow these integration instructions:

* [NPM](/spindl/techncial/javascript-sdk-html-script-guide)
* [HTML Script](/spindl/techncial/javascript-sdk-html-script-guide)
* [iOS](/spindl/techncial/ios-swift-sdk)
* [Android](/spindl/techncial/android-sdk)
  {% endtab %}

{% tab title="Send Events via API" %}
If a lot of the attribution events that you want to track are in your backend code, you can send us events via our API. Follow the instructions [here](/spindl/techncial/api).
{% endtab %}
{% endtabs %}

## Default vs Lite SDK versions

We offer 2 types of SDKs depending on 2 privacy modes depending on use cases. The vast majority of clients use `default` mode, but feel free to read more about the differences [here](/spindl/techncial/javascript-sdk-html-script-guide/default-vs-lite-sdk-versions).


# Google GTM Guide

To implement [GTM](https://marketingplatform.google.com/about/tag-manager/), you need to do 2 main steps:

### I. Add Spindl SDK as a Tag

1. In Your Tags Section, click `New`
2. In **Tag Configuration**, select `Custom Html`

<figure><img src="/files/n4haWtkAIZArD5p7kvCY" alt=""><figcaption></figcaption></figure>

3. Paste in Spindl Script below with the unique SDK Key that we provide to you. This code does 3 things:

* Installs spindl script
* Enables auto Page View events
* Enables auto Wallet Connect events
  * (we can only collect Wallet Connect events automatically for Metamask chrome extension. If you want to also capture Coinbase, OKX and other types wallets, then contact us & we will help set you up)

```html
<script src="https://cdn.spindl.xyz/attribution-1-8-1.js"></script>

<script>
 window.spindl.configure({
   sdkKey: "<Your SDK Key Here>" // use your personal SDK key here
  });
  
  window.spindl.enableAutoPageViews();
  window.spindl.enableAutoWalletConnects();
  
</script>
```

4. For **Triggering**, choose `Initialization - All Pages`

<figure><img src="/files/aydZPbYx34Y6dHLeAp2Z" alt=""><figcaption></figcaption></figure>

4. Click `Save` and don't forget to `Submit` the changes at the top right once you're ready.

Once submitted, you should shortly see `Page View` and `Wallet Connect` events in your Spindl Dashboard under the `Data` tab

<figure><img src="/files/F8xcqjKXVkmHLvCeGLZx" alt=""><figcaption></figcaption></figure>

### II. Track Custom Events

Now that the Spindl SDK has been instantiated and some events are flowing, you may want to track important custom events. These could be anything from button clicks to form submissions, navigation to specific areas, purchases, etc. To set this up, we need to follow 3 steps:

1. **Configure your Triggers:**&#x20;

* There are many ways to configure these that are outside the scope of Spindl. Feel free to watch some tutorials on YouTube to familiarize yourself with options if you're new to this

2. **Add Spindl Tag Template:**

* Go to `Templates` section and in `Tag Templates` section click on `Search Gallery`&#x20;
  * Find & Select `Spindl Tag Template`

<figure><img src="/files/z8QQcYQYyjTCP682qkCs" alt="" width="375"><figcaption></figcaption></figure>

<figure><img src="/files/hg9nDm6jEYKqt7z22XIi" alt="" width="375"><figcaption></figcaption></figure>

3. **Configure Your Triggers & Tags:**

* Go to the Tags Section and click on `New` to create a new Tag
* In `Tag Configuration` select `Spindl Tag Template` that you added in step 2.&#x20;
* Add the Event Name that you want to show up in the attribution
  * Event Type is by default  `Custom`. We may add more options here in the future

<figure><img src="/files/fqicCJXVt61ZdjdrbtVV" alt=""><figcaption></figcaption></figure>

* In `Triggering`, pick the triggers that you configured in Step 1 and click `Save`. You can `Preview` the changes (button in top right) before you submit the Workspace Changes.
  * After 10-20 seconds you should start seeing `Custom` events in the Spindl Data tab

    <figure><img src="/files/nQ98twW2SDihaa1cheHn" alt=""><figcaption></figcaption></figure>


# Javascript SDK / HTML Script Guide

## Steps

To implement the Javascript SDK/HTML Script, please use the following steps

1. [**Install**](/spindl/techncial/javascript-sdk-html-script-guide/install) **the library or script:** Using the NPM library or HTML script tag, you can initialize the SDK.
2. [**Setup**](/spindl/techncial/javascript-sdk-html-script-guide/setup-a-reverse-proxy) **a reverse proxy (optional):** We recommend setting up a reverse proxy so that events are less likely to be intercepted by tracking blockers. Works only with `npm` library implementation&#x20;
3. **Collect Events:** Start collecting [Wallet Connect](/spindl/techncial/javascript-sdk-html-script-guide/wallet-connects), [Page View](/spindl/techncial/javascript-sdk-html-script-guide/page-views), And [Custom](/spindl/techncial/javascript-sdk-html-script-guide/custom-events) events
4. [**Verify**](/spindl/techncial/javascript-sdk-html-script-guide/verify-that-events-are-sent-correctly) **that events are sent correctly**

You can also check the [examples](/spindl/techncial/javascript-sdk-html-script-guide/react-next.js-and-html-examples) section on how to implement with common frameworks like React and Next.js


# Install

## Authentication

Each customer of Spindl needs their own SDK Key to use the SDK. If you don't already have an SDK key, you can generate one on the [Settings](https://app.spindl.xyz/settings) page in the Spindl app.

## Installation via NPM/Yarn

The Spindl SDK is a lightweight npm package. You can install it below. Also here is the npm

* For differences between Default & Lite versions, you can check our [Default vs Lite SDK Versions](/spindl/techncial/javascript-sdk-html-script-guide/default-vs-lite-sdk-versions) section

{% tabs %}
{% tab title="Default\*" %}

```bash
npm install @spindl-xyz/attribution
yarn add @spindl-xyz/attribution
```

{% endtab %}

{% tab title="Lite\*" %}

<pre class="language-bash"><code class="lang-bash"><strong>npm install @spindl-xyz/attribution-lite
</strong>yarn add @spindl-xyz/attribution-lite
</code></pre>

{% endtab %}
{% endtabs %}

*\*Unsure of whether to use \`Default\` or `Lite`? The vast majority of clients use `default` but feel free to read more about the differences* [*here*](/spindl/techncial/javascript-sdk-html-script-guide/default-vs-lite-sdk-versions)*.*

#### Initialization via library

The Spindl SDK only needs to be configured once in a browser or Node session.

* If you're using Next.js, in page router, you can do this in `pages/_app.tsx`
  * If using app router, you need to implement it in a client side component
  * For example on how to implement in either one, checkout our [examples repo.](https://github.com/spindl-xyz/sdk-examples/tree/main)

{% tabs %}
{% tab title="Default" %}

<pre class="language-typescript"><code class="lang-typescript">import spindl from "@spindl-xyz/attribution";

<strong>spindl.configure({
</strong>  sdkKey: "&#x3C;your SDK API key here>",
  debugMode: true/false, // we recommend only to have debugMode=true when testing.
  // you will see console.logs of emitted events in browser
});
</code></pre>

{% endtab %}

{% tab title="Lite" %}

```typescript
import spindl from "@spindl-xyz/attribution-lite";

spindl.configure({
  sdkKey: "<your SDK API key here>",
  debugMode: true/false, // we recommend only to have debugMode=true when testing.
  // you will see console.logs of emitted events in browser
});
```

{% endtab %}

{% tab title="HTML Script" %}

```javascript
window.spindl.configure({
  sdkKey: "<your SDK API key here>",
  debugMode: true/false, // we recommend only to have debugMode=true when testing.
  // you will see console.logs of emitted events in browser
});
```

{% endtab %}
{% endtabs %}

## Installation via Script/CDN

Add async script tag at the top of html file within `head` tag. Please make sure you to use your sdk key.

{% tabs %}
{% tab title="Default" %}

```html
<head>
  ...
    <script
      async
      data-key="ADD_sdkKey_HERE"
      data-name="spindl-sdk"
      integrity="sha512-tnVaWexFbVZtEVlUBUMiWPwusxycBB3aDONgxC2zjX4CE0Tleo0zoLyI/JA6svx9SumV3KHGtAiD1mDrR+TpPg=="
      src="https://cdn.spindl.xyz/attribution-1-8-1.js"
      crossorigin="anonymous"
    ></script>
</head>
```

{% endtab %}

{% tab title="Lite" %}

```typescript
<head>
  ...
  <script
    async
    data-key="ADD_sdkKey_HERE"
    data-name="spindl-sdk"
    integrity="sha512-RYdSDx9Q1rcTihuBkGMeuW0c2U/PqL3tSuuvajN3Jp2UwgE9bMuL0bLvtbjPBMAzh573oPFOEwNdyi2sSPA6Kw=="
    src="https://cdn.spindl.xyz/attribution-lite-1-8-0.js"
    crossorigin="anonymous"
  ></script>
</head>
```

{% endtab %}
{% endtabs %}

#### Initialization via Script

Script automatically is instantiated and will track page view events & wallet connect events so you don't need to do anything

You will still be able to fire custom "Wallet Connects" & "Page Views" if you want by accessing spindl via window object. Example:

* For example on how to implement, checkout our [examples repo.](https://github.com/spindl-xyz/sdk-examples/tree/main)

```html
  <script>
    (async () => {
     async function manualPageView() {
      await window.spindl.pageView();
     }
     async function manualWalletConnect(address) {
      await window.spindl.attribute(address);
     }
    })();
  </script>
```


# Wallet Connects

This method should be called anytime a wallet is detected (on initial page load, wallet login, and wallet change):

{% tabs %}
{% tab title="NPM SDK" %}

```javascript
spindl.attribute(walletAddress: string);
```

{% endtab %}

{% tab title="HTML Script SDK" %}

```javascript
window.spindl.attribute(walletAddress: string);
```

{% endtab %}
{% endtabs %}

In most frameworks, this is as simple as adding a single line in the lifecycle hooks.

### Wagmi Tracking Wallet Connect

```typescript
import spindl from "@spindl-xyz/attribution";
// import spindl from "@spindl-xyz/attribution-lite" // only for lite version customers

import { useAccount } from "wagmi";

// ...
const Component = () => {
  const { address } = useAccount();

  useEffect(() => {
    if (address) {
      spindl.attribute(address);
    }
  }, [address]);
  // ...
};
```

### ThirdWeb Tracking Wallet Connect

```typescript
import spindl from "@spindl-xyz/attribution";
// import spindl from "@spindl-xyz/attribution-lite" // only for lite version customers

const address = useAddress();

React.useEffect(() => {
  if (address) {
    spindl.attribute(address);
  }
}, [address]);
```


# Page Views

Instrumenting Page View lets you visualize user flows and sessions within the Spindl App. These are optional, but give you more insight into user journeys and sources of traffic.

Spindl supports two modes for Page View tracking:

### 1. Automatic Page View Tracking

You can initialize this once in the app, and Spindl will log Page View events automatically:

{% tabs %}
{% tab title="NPM SDK" %}

```javascript
spindl.enableAutoPageViews();
```

{% endtab %}

{% tab title="HTML Script SDK" %}

<pre class="language-javascript"><code class="lang-javascript"><strong>window.spindl.enableAutoPageViews();
</strong></code></pre>

{% endtab %}
{% endtabs %}

* Check out our [examples repo](https://github.com/spindl-xyz/sdk-examples/tree/main) to see how it's implemented in different contexts in Next.js/React or HTML

### 2. Manual Page View Tracking

You can call this method manually on every page load.

{% tabs %}
{% tab title="NPM SDK" %}

```javascript
spindl.pageView();
```

{% endtab %}

{% tab title="HTML Script SDK" %}

```javascript
window.spindl.pageView();
```

{% endtab %}
{% endtabs %}


# Custom Events

You can track custom events across your application to gain a more detailed understanding of how users interact with it.

### Track

After you initial the Spindl SDK, you can call `track` on any interactions you want to track:

{% tabs %}
{% tab title="NPM SDK" %}

```javascript
import spindl from "@spindl-xyz/attribution";
// import spindl from "@spindl-xyz/attribution-lite" // only for lite version customers

// Track a basic event
spindl.track('form_submitted');

// Track events with optional properties
const additionalProperties = {
  value: 55,
  filterSelected: true,
  color: "red"
};

// optionally add user identity properties 
const identityProperties = {
  address: "0x1234...",
  customerUserId: "will@smith.com"
};

spindl.track('button_clicked', additionalProperties, identityProperties);
```

{% endtab %}

{% tab title="HTML Script SDK" %}

```html
<script>
// Track a basic event
window.spindl.track('form_submitted');

// Track events with optional properties
const additionalProperties = {
  value: 55,
  filterSelected: true,
  color: "red"
};

// optionally add user identity properties 
const identityProperties = {
  address: "0x1234...",
  customerUserId: "will@smith.com"
};

window.spindl.track('button_clicked', additionalProperties, identityProperties);

</script>
```

{% endtab %}
{% endtabs %}

**SDK Method**

To make data easily queryable, we have put the following restrictions on properties:

```typescript
spindl.track(
    event_name: string,
    event_properties?: Record<string, any>,
    identity_props?: {
        address?: string;
        customerUserId?: string;
    }
): void
```

**Arguments**

* **event\_name&#x20;*****(required)***: Min 3 & max 100 characters. Must be lower/uppercase alphanumeric. `_:-` symbols are allowed
* **event\_properties&#x20;*****(optional)*****:** Must be a valid JSON object. Max size 16KB & object keys & values cannot exceed 1,000 characters
* **identity\_props&#x20;*****(optional)*****:** You can pass in user props `address` and `customerUserId` to improve identity matching


# Verify That Events Are Sent Correctly

Once you start sending events, you can go to your organization's [Data](https://app.spindl.xyz/data) page of our web app to validate that events are being passed correctly

<figure><img src="/files/g5C0LTvWTVUctFZoZ6ZI" alt=""><figcaption></figcaption></figure>


# React, Next.js & Html Examples

For examples on how to implement the SDK in Next.js and in pure HTML, head over to our examples repo: <https://github.com/spindl-xyz/sdk-examples>


# Setup a Reverse Proxy

Tracking via a Reverse Proxy helps you send events using your own domain. As a result you are less likely to be intercepted by tracking blockers and be able to track more usage data.

Please use at least version **`1.6.0`** of `@spindl-xyz/attribution` or `@spindl-xyz/attribution-lite` for this.

&#x20;What happens under the hood that we forward the data to Spindl servers via a Reverse Proxy domain:

<figure><img src="/files/AuojvAeenmlUBwbNaiP9" alt=""><figcaption></figcaption></figure>

Here are several easy ways to implement this. Here are a few simple tutorials:

* [Next.js](/spindl/techncial/javascript-sdk-html-script-guide/setup-a-reverse-proxy/using-next.js)
* [Cloudflare](/spindl/techncial/javascript-sdk-html-script-guide/setup-a-reverse-proxy/using-cloudflare-workers)
* [Netlify](/spindl/techncial/javascript-sdk-html-script-guide/setup-a-reverse-proxy/netlify) (if you're using them as a hosting provider)

You can also use other services that support Reverse Proxies. Here are a a few popular hosting providers and their documentation:

* Amazon Web Services: [API Gateway](https://aws.amazon.com/api-gateway/resources/)
* Microsoft Azure: [API Management](https://docs.microsoft.com/en-us/azure/api-management/api-management-key-concepts)
* Google Cloud: [API Gateway](https://cloud.google.com/api-gateway/docs)


# Using Next.js

*Please use at least version **`1.6.0`** of* `@spindl-xyz/attribution` *or* `@spindl-xyz/attribution-lite` *for this feature.*

## 1. Add Next.js Code

### Option 1) Using Rewrites as  proxy

Add `ingest` route that using `rewrites()` method forwards the data to `https://spindl.link/` in your next.config file

```javascript
// next.config.js OR next.config.ts
const nextConfig = {
  async rewrites() {
    return [
      {
        source: "/ingest/:path*",
        destination: "https://spindl.link/:path*",
      },
    ];
  },
  // ...
}
module.exports = nextConfig
```

This should work! If you're experiencing any issues, Option 2 is an alternative!

### Option 2)  Use Custom Middleware using App Router

If option 1 doesn't work, you can write a custom middleware `middleware.js/ts` in your base directory (your app folder) to point `/ingest` url to our `spindl.link` hostname

```javascript
import { NextResponse } from "next/server";

export function middleware(request: any) {
  const hostname = "spindl.link";

  const requestHeaders = new Headers(request.headers);
  requestHeaders.set("host", hostname);

  let url = request.nextUrl.clone();
  url.protocol = "https";
  url.hostname = hostname;
  url.port = 443;
  url.pathname = url.pathname.replace(/^\/ingest/, "");

  return NextResponse.rewrite(url, {
    headers: requestHeaders,
  });
}

export const config = {
  matcher: "/ingest/:path*",
};
```

## 2. Configure Spindl SDK host with the proxy

Add `host`  url to spindl configure object with the proxy host url

```javascript
spindl.configure({
  sdkKey: "<your SDK API key here>",
  host: "https://your-host-url.com/ingest"
});
```

You can use the `window.location.origin`  as part of host url but in SSR contexts like Next.js you may need to check if `window` is defined first

```javascript
if (typeof window !== "undefined") {
  spindl.configure({
    sdkKey: process.env.NEXT_PUBLIC_SPINDL_SDK_KEY as string,
    host: `${window.location.origin}/ingest`,
  });
}
```


# Using Cloudflare Workers

*Please use at least version **`1.6.0`** of* `@spindl-xyz/attribution` *or* `@spindl-xyz/attribution-lite` *for this feature.*

Make sure you're logged into [cloudflare](https://www.cloudflare.com/). Workers are really easy to set up and allow up to 100,000 **free** requests per day on the [free plan](https://developers.cloudflare.com/workers/platform/pricing/).&#x20;

### 1. Create a worker <a href="#id-1-create-a-worker" id="id-1-create-a-worker"></a>

From Cloudflare dashboard, select:  `Workers & Pages` > `Overview` > `Create application` > `Create Worker`. At this point, you can either keep the random worker name or choose your own. Click `Deploy` once done.

### 2. Configure as proxy

Click `Edit code` or `Quick edit` once the worker is set up and paste the code. You should now be seeing a code editor for the worker. You can copy/paste code below which will forwards the traffic and data to our server on `spindl.link`

```javascript
const API_HOST = "spindl.link"

async function handleRequest(event) {
    const url = new URL(event.request.url)
    const pathname = url.pathname
    const search = url.search
    const pathWithParams = pathname + search
    return forwardRequest(event, pathWithParams)
}

async function forwardRequest(event, pathWithSearch) {
    const request = new Request(event.request)
    return await fetch(`https://${API_HOST}${pathWithSearch}`, request)
}

addEventListener("fetch", (event) => {
    event.passThroughOnException()
    event.respondWith(handleRequest(event))
})
```

When done, click "Save and deploy".

### 3. Configure Spindl SDK to use Proxy domain

Copy the `xxx.workers.dev` domain that you just created! Please make sure that domain doesn't include any common words that are more likely to be blocked by ad blockers including `track`, `analytics` and `spindl`

In the sdindl sdk configure object, paste the worker url as the `host`:

```javascript
spindl.configure({
  sdkKey: "<your SDK API key here>",
  host: "https://your-worker-url.workers.dev"
});
```


# Netlify

*Please use at least version **`1.6.0`** of* `@spindl-xyz/attribution` *or* `@spindl-xyz/attribution-lite` *for this feature.*

If you're using Netlify hosting service, they provide easy [redirects](https://docs.netlify.com/routing/redirects/#syntax-for-the-redirects-file) that can be used as a reverse proxy as well.&#x20;

### Add netlify.toml file

```javascript
[[redirects]]
  from = "/ingest/*"
  to = "https://spindl.link/:splat"
  host = "spindl.link"
  status = 200
  force = true
```

### Configure Spindl SDK with the proxy

Once that is done, add `host` to spindl configure object with the proxy host url

```javascript
spindl.configure({
  sdkKey: "<your SDK API key here>",
  host: "https://your-host-url.com/ingest"
});
```

Once completed, deploy the changes to Netlify and ensure the Spindl events are being sent via the `/ingest` endpoint by checking the network tab of your browser

You can use the `window.location.origin`  as part of host url but in SSR contexts like Next.js you may need to check if `window` is defined first

```javascript
if (typeof window !== "undefined") {
  spindl.configure({
    sdkKey: process.env.NEXT_PUBLIC_SPINDL_SDK_KEY as string,
    host: `${window.location.origin}/ingest`,
  });
}
```


# Referrals

Referral codes are a core primitive of Spindl's referral program. Each wallet can create one unique code, that can be shared via the `ref` tag on URLs. Codes can be custom or random, depending on your ideal app experience.

For instance, a shareable link might look:

```
https://app.mydapp.xyz/trade?ref=kunal
                                 ^ referral code
```

The Spindl SDK will automatically capture `ref` URL parameters in Page View events to associate visits with Referrers.​

Referral codes can be created manually through the [Spindl Dashboard](https://app.spindl.xyz/referrals), or programmatically from your app with the Javascript SDK.​

### &#x20;<a href="#get-referrer-code" id="get-referrer-code"></a>

### Get Referrer Code <a href="#get-referrer-code" id="get-referrer-code"></a>

This method can be used to see if a wallet already has a code associated with it.

**SDK Method**

```typescript
getReferrerCode(address: string): Promise<string | undefined>
```

**Parameters**

* Address: The wallet address

**Returns**

A referral code if set​

### &#x20;<a href="#create-referrer-code" id="create-referrer-code"></a>

### Create Referrer Code <a href="#create-referrer-code" id="create-referrer-code"></a>

**SDK Method**

```typescript
createReferrerCode(address: string, code?: string): Promise<string>
```

**Parameters**

* Address: The wallet address
* Code: An optional custom referral code to use. If not set, we'll generate a random code

**Returns**

This will throw a 400 error if the code is invalid / in-use, or the wallet address already has a referral code

#### **​** <a href="#undefined" id="undefined"></a>

### **Validate Referrer Code** <a href="#validate-referrer-code" id="validate-referrer-code"></a>

This method can be used to quickly verify if a code is valid (it doesn't make network calls, so can be called on keystrokes / input changes efficiently)

**SDK Method**

```typescript
validateReferrerCode(code: string): boolean;
```

**Parameters**

* Code: A custom short code, to validate

**Returns**

Returns true if the code is valid.

### Check Referrer Code

This method can be used to see if a custom code is already in use.

**SDK Method**

```typescript
checkReferrerCode(code: string): Promise<boolean>;
```

**Parameters**

* Code: A custom short code, to check if it is available

**Returns**

Returns true if the code is available, false if it is not valid or unavailable


# Default vs Lite SDK Versions

The Spindl SDK is available in two privacy modes:

* **Default**: The default Spindl SDK behaves the same exact same way as other major analytics providers, whether Google Analytics, Twitter, or large attribution platforms like Branch or AppsFlyer: a combination of cookies and calculated device identifiers roughly unify a user journey across many different apps and experiences.
* **Lite**: In our ‘privacy safe’ version, we intentionally assign a transient device identifier to any Web 2 user touchpoint, refreshing the pseudonym regularly and essentially deleting any persistent data about that device. This is how Plausible and other privacy-aware services popular among some Web 3 projects work.

Both SDKs have the same API, and can be substituted between each other. The only difference is which package to install and import.

{% hint style="info" %}
Unsure which model to choose? Reach out and we can help you find the right solution for your app and your customers.
{% endhint %}


# Security

Security for our Javascript SDK is top of mind given the potential downstream effects that are possible if things were to go wrong. Here are the ways we work together with clients to greatly reduce likelihood of anything terrible happening

## NPM library

### Pin to Exact Version

* Pin the spindl sdk npm dependency to an [exact version](https://docs.npmjs.com/cli/v8/commands/npm-install#save-exact). This way, even if our npm account gets hijacked and a malicious minor patch version is released to npm registry, you will not get the updated malicious version. You can do this in the following ways:
  * &#x20;`yarn add --exact @spindl-xyz/attribution@1.8.1`
  * `npm i --save-exact @spindl-xyz/attribution@1.8.1`

If you want to take a look and audit the sdk code yourself, feel free to reach out to us and we will provide access.

## Script library

### Include Integrity Hash

Our Script Installation [instruction](/spindl/techncial/javascript-sdk-html-script-guide/install#installation-via-script-cdn) include an integrity hash that must be added to the script to ensure top security. In the scenario that if our AWS account somehow gets hacked and malicious script is added to our S3 bucket, the [subresouce integrity](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity) verification will fail because the hash differs from the hash that that would be generated by the malicious code. Below is an example:

```markup
<script
      async
      data-key="ADD_sdkKey_HERE"
      data-name="spindl-sdk"
      integrity="sha512-tnVaWexFbVZtEVlUBUMiWPwusxycBB3aDONgxC2zjX4CE0Tleo0zoLyI/JA6svx9SumV3KHGtAiD1mDrR+TpPg=="
      src="https://cdn.spindl.xyz/attribution-1-8-1.js"
      crossorigin="anonymous"
></script>
```

### Download the Script & Host Internally

If you want extra security, you can download the code from our script url (i.e. <https://cdn.spindl.xyz/attribution-1-6-0.js>), host the script yourself locally and reference it in the script tag. This way you're not relying on our AWS S3 bucket or npm hosting registry.

```markup
<script
      async
      data-key="ADD_sdkKey_HERE"
      data-name="spindl-sdk"
      src="../location/of-local-spindl-script.js"
      crossorigin="anonymous"
></script>
```

## Internal Best Development Practices

1. Our SDK does not have any production npm dependencies and we have 0 known vulnerabilities on our dev dependencies when we run `npm audit`
2. We have multi factor auth on the important aspects that touch the browser SDK


# API

Spindl's Server-to-Server API provides access to campaign and attribution management, and allows easy exporting of data and reports.

## API Key

If you don't already have an API key, you can generate one on the [Settings](https://app.spindl.xyz/settings) page in the Spindl app.

{% hint style="warning" %}
API keys are different from your SDK keys. Please be sure to keep your API access tokens secure and private! Do not share them in emails, client-side code or publicly accessible sites.

If you have accidentally shared an API access token publicly, you can revoke it in your [Settings](https://app.spindl.xyz/settings) by clicking the X button beside the token.
{% endhint %}

## Authentication

API requests are authenticated using the `X-API-Key` header. With the API token from the previous step, calling the Spindl API is as simple as:

```bash
curl -H "X-API-Key: <your_api_key>" https://api.spindl.xyz/v1/links
```

## Errors

Spindl uses HTTP response codes to indicate the success or failure of an API request.

* Codes in the `2xx` range indicate success
* Codes in the `4xx` range indicate incorrect or incomplete requests (e.g. a required parameter was omitted)
* Codes in the `5xx` range indicate an error on Spindl's servers.

On error responses, Spindl also provides error messages and code in the response body:

```json
{
    "statusCode": 404,
    "message": "link not found"
}
```


# Short Links

Redirect links are one of the core primitives on Spindl. They map a Spindl link (`https://spindl.link/abcd`) to a specified URL (typically a dApp website).

## Get all links for your organization

<mark style="color:blue;">`GET`</mark> `https://api.spindl.xyz/v1/links`

Links will sorted by the time they were created, descending.

#### Headers

| Name                                        | Type   | Description   |
| ------------------------------------------- | ------ | ------------- |
| X-API-Key<mark style="color:red;">\*</mark> | String | The API token |

{% tabs %}
{% tab title="200: OK OK" %}

```json
{
  "links": [
    {
      "id": "dfec5988-5108-465c-be87-ce42039f572e",
      "name": "Spindl Campaign - 2022 - Discord",
      "link": "https://spindl.link/bcdef",
      "redirectUrl": "https://demo.spindl.xyz/",
      "createdAt": "2022-11-09T22:56:24.303Z",
      "totalVisits": 0,
      "latestVisit": null
    },
    {
      "id": "7c087843-a7a1-4b2c-9f28-2ccbe40f23e5",
      "name": "Spindl Campaign - 2022 - Twitter",
      "link": "https://spindl.link/abcd",
      "redirectUrl": "https://demo.spindl.xyz/",
      "createdAt": "2022-11-04T00:29:49.810Z",
      "totalVisits": 100,
      "latestVisit": "2022-11-06T17:32:12.632Z"
    }
  ]
}
```

{% endtab %}
{% endtabs %}

## Create a link

<mark style="color:green;">`POST`</mark> `https://api.spindl.xyz/v1/links`

Create a new short link with Spindl tracking.

#### Headers

| Name      | Type   | Description   |
| --------- | ------ | ------------- |
| X-API-Key | String | The API token |

#### Request Body

| Name                                   | Type   | Description                                                                                                                            |
| -------------------------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------- |
| name<mark style="color:red;">\*</mark> | String | The internal name of the link. While names aren't unique, we recommend using descriptive names like "Winter Campaign - Twitter Post 1" |
| url<mark style="color:red;">\*</mark>  | String | The URL to redirect to (typically where your app lives).                                                                               |

{% tabs %}
{% tab title="200: OK OK" %}

```javascript
{
  "link": {
    "id": "dfec5988-5108-465c-be87-ce42039f572e",
    "name": "Test New 2",
    "link": "https://spindl.link/abcde",
    "redirectUrl": "https://demo.spindl.xyz/",
    "createdAt": "2022-11-09T22:56:24.303Z",
    "totalVisits": 0,
    "latestVisit": null
  }
}
```

{% endtab %}
{% endtabs %}

##


# Custom Events API

For more detailed understanding of how users interact with your application, you can track custom events across your application by sending us Custom Events via our API at <https://spindl.link/events/server>.

**To do this, you will need:**

* Spindl API Key
* Properly Formatted events (you can send individual event or an array)
  * for each event, you can define an object:

<pre class="language-json"><code class="lang-json"><strong>[
</strong><strong>  {
</strong>   "type": "CUSTOM", // required (use the default "CUSTOM")
   "data": {
     "name": "Your Event Name", // required.
     "properties": { ... } // optional data can be passed as JSON here
   },
   "identity": {
      // either address or customerUserId is required to help us identify a user
      "address": "0x...",
      "customerUserId": "test@gmail.com"
   }
  }
]
</code></pre>

Below are several examples for sending an **array** of events to our API:

## Send Custom Events

<mark style="color:green;">`POST`</mark> `https://spindl.link/events/server`

Sending Custom events via API

#### Headers

| Name                                        | Type   | Description   |
| ------------------------------------------- | ------ | ------------- |
| X-API-Key<mark style="color:red;">\*</mark> | String | The API token |

#### Request Body

| Name                               | Type  | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| ---------------------------------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| <mark style="color:red;">\*</mark> | Array | <p>array of events is required to be passed in. Please pass in a array of objects with the following keys:<br><br><strong>type (required): string</strong></p><p>use <strong>"CUSTOM"</strong> here<br><br><strong>name </strong><em><strong>(required)</strong></em>: <strong>string</strong></p><p> Min 3 & max 100 characters. Must be lower/uppercase alphanumeric. <code>\_:-</code> symbols & spaces are allowed</p><p></p><p><strong>properties </strong><em><strong>(optional)</strong></em><strong>: Object</strong></p><p>Must be a valid JSON object. Max size 16KB & object keys & values cannot exceed 1,000 characters</p><p></p><p><strong>identity: {</strong><br>   <strong>address: string </strong><em><strong>(required)</strong></em></p><p>   <strong>customerUserId: string </strong><em><strong>(required)</strong></em><br><strong>}</strong> : either <code>address</code> or <code>customerUserId</code> is required to help us identify a user. </p><p></p><p><code>address</code> must be a valid wallet address. </p><p><code>cusomerUserId</code> can be a unique db user id or a unique identifier like email address.</p><p></p><p>These identifiers are very important part of stitching identities and providing accurate attribution.<br><br></p> |

{% tabs %}
{% tab title="204: No Content OK" %}

{% endtab %}
{% endtabs %}

<details>

<summary>NodeJs Axios Example</summary>

```javascript
const axios = require('axios');
let data = JSON.stringify([
  {
    "type": "CUSTOM",
    "data": {
      "name": "ADD_TO_CART",
      "properties": {
        "optional_data": "here"
      }
    },
    "identity": {
      "address": "0x0000000000000000000000000000000000000000",
      "customerUserId": "test@gmail.com"
    }
  },
  {
    "type": "CUSTOM",
    "data": {
      "name": "TEST_EVENT_2",
      "properties": {
        "optional_data": "here"
      }
    },
    "identity": {
      "address": "0x0000000000000000000000000000000000000000"
    }
  }
]);

let config = {
  method: 'post',
  url: 'https://spindl.link/events/server',
  headers: { 
    'X-Api-Key': 'YOUR_API_KEY', 
    'Content-Type': 'application/json',
  },
  data : data
};

axios.request(config)
.then((response) => {
  console.log(JSON.stringify(response.data));
})
.catch((error) => {
  console.log(error);
});


```

</details>

<details>

<summary>Javascript Fetch Example</summary>

```javascript
var myHeaders = new Headers();
myHeaders.append("X-Api-Key", "YOUR_API_KEY");
myHeaders.append("Content-Type", "application/json");

var raw = JSON.stringify([
  {
    "type": "CUSTOM",
    "data": {
      "name": "ADD_TO_CART",
      "properties": {
        "optional_data": "here"
      }
    },
    "identity": {
      "address": "0x0000000000000000000000000000000000000000",
      "customerUserId": "test@gmail.com"
    }
  },
  {
    "type": "CUSTOM",
    "data": {
      "name": "TEST_EVENT_2",
      "properties": {
        "optional_data": "here"
      }
    },
    "identity": {
      "address": "0x0000000000000000000000000000000000000000",
      "customerUserId": "test@gmail.com"
    }
  }
]);

var requestOptions = {
  method: 'POST',
  headers: myHeaders,
  body: raw,
  redirect: 'follow'
};

fetch("https://spindl.link/events/server", requestOptions)
  .then(response => response.text())
  .then(result => console.log(result))
  .catch(error => console.log('error', error));
```

</details>

<details>

<summary>cURL Example</summary>

```powershell
curl --location 'https://spindl.link/events/server' \
--header 'X-Api-Key: YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '[
    {
        "type": "CUSTOM",
        "data": {
            "name": "ADD_TO_CART",
            "properties": {
                "optional_data": "here"
            }
        },
        "identity": {
            "address": "0x0000000000000000000000000000000000000000",
            "customerUserId": "test@gmail.com"
        }
    },
    {
        "type": "CUSTOM",
        "data": {
            "name": "TEST_EVENT_2",
            "properties": {
                "optional_data": "here"
            }
        },
        "identity": {
            "address": "0x0000000000000000000000000000000000000000"
        }
    }
]'
```

</details>


# Data Exports

Spindl data export lets you get a full daily dump of all your client and blockchain events to a shared AWS S3 bucket. The dump is daily, and delayed about 8 hours to allow enough time for proces

Below is the currently supported schema, if you need something specific not listed here please reach out to us!

**Schema**

| Category            | Field Name                | Description                    |
| ------------------- | ------------------------- | ------------------------------ |
| Basic Data          | event\_id                 | Unique id of the event         |
|                     | event\_name               |                                |
|                     | event\_type               |                                |
|                     | event\_time               |                                |
|                     | url                       |                                |
|                     | url\_params               |                                |
|                     | path\_name                |                                |
| Session Info        | session\_id               |                                |
| Identity            | customer\_user\_id        | (If provided in events)        |
|                     | identity\_wallet\_address |                                |
| Browser Information | device\_user\_agent       | User agent of browser          |
|                     | utm\_medium               |                                |
|                     | utm\_source               |                                |
|                     | utm\_campaign             |                                |
|                     | utm\_content              |                                |
|                     | utm\_term                 |                                |
|                     | referer                   | Page Referer (from Browser)    |
| Onchain Data        | chain\_id                 |                                |
|                     | tx\_hash                  |                                |
| Attribution Data    | attribution\_channel      |                                |
|                     | attribution\_type         | enum (organic, external, etc.) |

**Technical Details**

* **Method 1: Spindl Access to your S3 Bucket**

  * You’ll need to give an S3 bucket to the following ARN:

    ```jsx
    arn:aws:iam::475852047645:role/spindl-data-exports-role
    ```

  * For example, a bucket policy might look like:

    ```
    {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Principal": {
                    "AWS": "arn:aws:iam::475852047645:role/spindl-data-exports-role"
                },
                "Action": [
                    "s3:GetObject",
                    "s3:GetObjectVersion",
                    "s3:PutObject"
                ],
                "Resource": "arn:aws:s3:::{bucket-name}/*"
            },
            {
                "Effect": "Allow",
                "Principal": {
                    "AWS": "arn:aws:iam::475852047645:role/spindl-data-exports-role"
                },
                "Action": "s3:GetBucketLocation",
                "Resource": "arn:aws:s3:::{bucket-name}"
            },
            {
                "Effect": "Allow",
                "Principal": {
                    "AWS": "arn:aws:iam::475852047645:role/spindl-data-exports-role"
                },
                "Action": "s3:ListBucket",
                "Resource": "arn:aws:s3:::{bucket-name}",
                
            }
        ]
    }
    ```

* **Method 2:  Spindl IAM Role on your AWS Account**
  * Create IAM User on your side with read access to the s3 bucket and provide us the *AWS\_ACCESS\_KEY* and *AWS\_SECRET\_KEY*

File Format and Details

* Format: .csv and .parquet files are currently supported
* Each daily file will be formatted as {YYYY-MM-DD}.csv where the date is the start date
* First file will upload a backfill, and future files will follow the above format


# Android SDK

### Install Using **Android Studio**

1. Open the Project Structure dialog (`File > Project Structure...`).
2. Choose `Dependencies` from the left pane, then the main module (e.g. `app`).
3. Click the `+` button in the `Declared Dependencies` pane, and choose `1 Library Dependency` from the popup menu to open the `Add Library Dependency` dialog.
4. In the `Add Library Dependency` dialog, type `xyz.spindl` under Step 1 and press the `Search` button.
5. Choose the `xyz.spindl` | `sdk` result from the results table, and the highest version available.
6. Make sure `implementation` is selected under Step 2, and press `OK`.

### Initialize SDK

Setup a Spindl instance, e.g. in `MainActivity`:

```kotlin
class MainActivity : ComponentActivity() {
    private lateinit var spindl: Spindl

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val apiKey = getString(R.string.spindlApiKey)
        lifecycleScope.launch {
            spindl = Spindl(context = applicationContext, lifecycleOwner = this@MainActivity)
            spindl.initialize(apiKey = apiKey)
        }
    ...
```

### Track Events

Track events with the `track` method:

* `name` is required
* `properties` are optional

```kotlin
val properties = JsonObject()
properties.addProperty("prop1Key", "prop1Value")

spindl.track("eventName", properties)
```

### Identify Users

Once customers enter their customer ID (such as email) and/or wallet information (e.g., after logging in), connect that to the analytics by calling the `identify` method:

```kotlin
spindl.identify(walletAddress = wallet, customerUserId = userId)
```


# iOS (Swift) SDK

### Install Using **Swift Package Manager (requires Xcode 12+)**

1. In Xcode, select File > Swift Packages > Add Package Dependency.
2. Paste the URL [`https://github.com/spindl-xyz/spindl-ios/`](https://github.com/spindl-xyz/spindl-ios/) and a minimum semantic version of v1.0.1. You can see in the [tags sections](https://github.com/spindl-xyz/spindl-ios/tags), our latest releases.

### Initialize SDK

Initialize the SDK with your API key upon launching the app, usually in the `AppDelegate`, or `App` protocol implementor:

```swift
import SpindlSDK

struct MyApp : App {

  init() {
    Spindl.initialize("<your API key goes here>")
  }
  
  ...
}
```

### Track Events

Track events with the `track` method:

* `name` is required
* `properties` are optional

```swift
private func myButtonTappedExample() async throws {
    try await Spindl.shared.track(name: "myButtonTapped", properties: ["view":"MyFancyView","otherProperty":"Another one"])
    ...
}
```

### Identify Users

Once customers enter their customer ID (such as email) and/or wallet information (e.g., after logging in), connect that to the analytics by calling the `identify` method of the `Spindl` singleton:

```swift
private func saveUserIdentityExample(wallet: String?, email: String?) async throws {
    try await Spindl.shared.identify(walletAddress: wallet, customerUserId: email)
    ...
}
```


# On-Chain

To manage incentives and on-chain rewards, Spindl uses smart contracts to pay out rewards.

You can see more technical details here:

{% content-ref url="/pages/jMuiCHHyJ3dQYjlGjTMN" %}
[Technical Details](/spindl/features/referrals/technical-details)
{% endcontent-ref %}


# Rewards

After a successful referral, Spindl will automatically credit wallets with rewards in an on-chain smart contract. The smart contract provides methods to directly to check the status of rewards for a particular wallet and facilitate a withdrawal transaction on-chain.

{% hint style="info" %}
The Smart Contract address and Campaign ID to use in the methods below are specific to your running campaigns, and available within the Spindl Dashboard.

To test, you can use the [0xEC16738B5c89837e90f117dEAb1e9743Be562d69](https://goerli.arbiscan.io/address/0xEC16738B5c89837e90f117dEAb1e9743Be562d69) on Arbitrum Goerli Test Net, with Campaign ID `1`(details [below](#test-contract)).
{% endhint %}

{% hint style="info" %}
All methods below are Smart Contract calls, that can be called via a library like Ethers.js
{% endhint %}

### Check Available Rewards

**RPC Call**

```solidity
getRecipient(_campaignId, _recpientAddress) public view returns (RecipientDetails memory) 
```

([Etherscan Reference](https://goerli.arbiscan.io/address/0xEC16738B5c89837e90f117dEAb1e9743Be562d69#readProxyContract#F3))

**Parameters**

* *Campaign ID*: This is the campaign id associated with the campaign you are running. If you are running multiple campaigns, you can check for rewards for each one individually.
* *Recipient Address*: Logged in wallet.

**Response**

```solidity
struct RecipientDetails {
    RecipientStatus status;
    uint256 earned;
    uint256 withdrawn;
}

enum RecipientStatus {
    NONE,
    ACTIVE,
    PAUSED
}
```

(*Note*: The status will almost always be NONE or ACTIVE. The PAUSED status is a special flag that only you can trigger, for suspicious referrers)

**Examples**

<details>

<summary>Wallet Has Pending Rewards</summary>

Recipient 0x0000000000000000000000000000000000000021 has been credited some rewards. The return value is:

```
[1,1200000000,0]
```

The status is ACTIVE (1), which means the recipient is eligible to withdraw funds. They have earned `1200000000` wei, and withdrawn `0` wei, so they are eligible to withdraw `1200000000 - 0 = 1200000000`

</details>

<details>

<summary>Wallet Has No Rewards</summary>

Recipient 0x0000000000000000000000000000000000000000 has never been rewarded. The return value is:

`[0, 0, 0`]

The status is NONE (0), which means the recipient address has never been credited. The earned and withdrawn values are 0 (since there is nothing owed to them).

</details>

<details>

<summary>Wallet Has Withdrawn All Rewards</summary>

Recipient 0xD4ed4369ae2c27924ee59bd49459C642C20395B2 has withdrawn all rewards. In this case, the return value is:

`[0,1200000000,1200000000]`

The status is ACTIVE (1), which means the recipient is eligible to withdraw funds. They have earned `1200000000` wei, and withdrawn `1200000000` wei, so there are no remaining funds left to withdraw.

</details>

### Withdraw Rewards

**Transaction Call**

```solidity
withdrawRecipientEarnings(uint32 _campaignId, uint256 _amount, address _to) public virtual
```

([Etherscan Reference](https://goerli.arbiscan.io/address/0xEC16738B5c89837e90f117dEAb1e9743Be562d69#writeProxyContract#F15))

**Parameters**

* *Campaign ID*: This is the campaign id associated with the campaign you are running.
* *Amount*: The amount to withdraw. This is usually the total withdrawal amount, but can be a smaller amount as well. This will be in the currency for the campaign.&#x20;
* *To*: The recipient of the funds. This is usually the address of the wallet calling this function, but you can set it to another address specific by the user

**Returns**

This is a transaction, so there will be no return type. The transaction should be successful if the caller is owed at least that amount in the campaign.

### Campaign Details

**RPC Call**

```solidity
campaigns(_campaignId) public view returns (totalBudget uint256, status uint8, paymentType address, totalEarned uint256)
```

([Etherscan Reference](https://goerli.arbiscan.io/address/0xEC16738B5c89837e90f117dEAb1e9743Be562d69#readProxyContract#F2))

#### Parameters

* *Campaign ID*: This is the campaign id associated with the campaign you are running.

**Returns**

* Total Budget: The total budget set by the owner of the campaign
* Status
* Payment Type: The ERC20 token, or 0x0... for the native currency&#x20;
* Total Earned: The total amount of token allocated referrers and referees.

### Test Contract

Here is an example contract on the Arbitrum Tesnet:

* Address: 0xEC16738B5c89837e90f117dEAb1e9743Be562d69 ([URL](https://goerli.arbiscan.io/address/0xEC16738B5c89837e90f117dEAb1e9743Be562d69#readProxyContract))
* Campaign ID: 1

We populated the campaign with earnings for these referrer addresses to make testing easier:

```typescript
0xD4ed4369ae2c27924ee59bd49459C642C20395B2
0x0000000000000000000000000000000000000021
0x0000000000000000000000000000000000000022
0x0000000000000000000000000000000000000023
0x0000000000000000000000000000000000000024
0x0000000000000000000000000000000000000025
0x0000000000000000000000000000000000000026
0x0000000000000000000000000000000000000027
0x0000000000000000000000000000000000000028
0x0000000000000000000000000000000000000029
0x0000000000000000000000000000000000000030
```

### Contract ABI

You can use this to interact with the contract with any Web3 client libraries, like Ethers.js

```
[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"BadAddress","type":"error"},{"inputs":[],"name":"BadPayment","type":"error"},{"inputs":[],"name":"BadRequest","type":"error"},{"inputs":[],"name":"ImproperCampaignState","type":"error"},{"inputs":[],"name":"IncorrectAmount","type":"error"},{"inputs":[],"name":"OverAttributed","type":"error"},{"inputs":[],"name":"PaymentTypeDoesNotExist","type":"error"},{"inputs":[],"name":"Unauthorized","type":"error"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint32","name":"campaignId","type":"uint32"},{"indexed":false,"internalType":"uint256","name":"withdrawAmount","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"newTotalBalance","type":"uint256"}],"name":"BalanceWithdrawn","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint32","name":"campaignId","type":"uint32"},{"indexed":false,"internalType":"address","name":"paymentType","type":"address"}],"name":"CampaignCreated","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint32","name":"campaignId","type":"uint32"},{"indexed":false,"internalType":"uint256","name":"updatedBudget","type":"uint256"}],"name":"CampaignFunded","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint32","name":"campaignId","type":"uint32"},{"indexed":false,"internalType":"enum PayoutVault.CampaignStatus","name":"oldState","type":"uint8"},{"indexed":false,"internalType":"enum PayoutVault.CampaignStatus","name":"newState","type":"uint8"}],"name":"CampaignStatusUpdated","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint8","name":"version","type":"uint8"}],"name":"Initialized","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"oldAddress","type":"address"},{"indexed":false,"internalType":"address","name":"newAddress","type":"address"}],"name":"ManagerAddressUpdated","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"previousOwner","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"paymentAddress","type":"address"}],"name":"PaymentAdded","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint32","name":"campaignId","type":"uint32"},{"indexed":false,"internalType":"address","name":"recipientAddress","type":"address"}],"name":"RecipientAdded","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint32","name":"campaignId","type":"uint32"},{"indexed":true,"internalType":"address","name":"addr","type":"address"},{"indexed":false,"internalType":"enum PayoutVault.RecipientStatus","name":"status","type":"uint8"}],"name":"RecipientStatusUpdated","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint32","name":"campaignId","type":"uint32"},{"indexed":true,"internalType":"address","name":"recipientAddress","type":"address"},{"indexed":false,"internalType":"address","name":"paymentType","type":"address"},{"indexed":false,"internalType":"uint256","name":"newEarnings","type":"uint256"}],"name":"UpdateCompleted","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint32","name":"campaignId","type":"uint32"},{"indexed":true,"internalType":"address","name":"recipientAddress","type":"address"},{"indexed":false,"internalType":"address","name":"paymentType","type":"address"},{"indexed":false,"internalType":"uint256","name":"totalWithdrawn","type":"uint256"},{"indexed":false,"internalType":"enum PayoutVault.RecipientWithdrawType","name":"withdrawType","type":"uint8"}],"name":"WithdrawSuccess","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"oldAddress","type":"address"},{"indexed":false,"internalType":"address","name":"newAddress","type":"address"}],"name":"WorkerAddressUpdated","type":"event"},{"inputs":[{"internalType":"address","name":"_address","type":"address"}],"name":"addERC20Payment","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint32","name":"_campaignId","type":"uint32"},{"internalType":"address[]","name":"_recipientAddresses","type":"address[]"}],"name":"addRecipients","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"campaignCounter","outputs":[{"internalType":"uint32","name":"","type":"uint32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint32","name":"","type":"uint32"}],"name":"campaigns","outputs":[{"internalType":"uint256","name":"totalBudget","type":"uint256"},{"internalType":"enum PayoutVault.CampaignStatus","name":"status","type":"uint8"},{"internalType":"address","name":"paymentType","type":"address"},{"internalType":"uint256","name":"totalEarned","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_paymentType","type":"address"},{"internalType":"address[]","name":"_recipientAddresses","type":"address[]"}],"name":"createCampaign","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint32","name":"_campaignId","type":"uint32"},{"internalType":"uint256","name":"_amount","type":"uint256"}],"name":"fundCampaign","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint32","name":"_campaignId","type":"uint32"},{"internalType":"address","name":"_recipientAddress","type":"address"}],"name":"getRecipient","outputs":[{"components":[{"internalType":"enum PayoutVault.RecipientStatus","name":"status","type":"uint8"},{"internalType":"uint256","name":"earned","type":"uint256"},{"internalType":"uint256","name":"withdrawn","type":"uint256"}],"internalType":"struct PayoutVault.RecipientDetails","name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_owner","type":"address"},{"internalType":"address","name":"_managerAddress","type":"address"},{"internalType":"address","name":"_workerAddress","type":"address"},{"internalType":"address[]","name":"_paymentTypes","type":"address[]"}],"name":"initialize","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"managerAddress","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"paymentTypes","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint32","name":"_campaignId","type":"uint32"},{"internalType":"address[]","name":"_recipientAddresses","type":"address[]"}],"name":"pushRecipientEarnings","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"renounceOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint32","name":"_campaignId","type":"uint32"},{"internalType":"enum PayoutVault.CampaignStatus","name":"_status","type":"uint8"}],"name":"setCampaignStatus","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_newAddress","type":"address"}],"name":"setManagerAddress","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint32","name":"_campaignId","type":"uint32"},{"internalType":"address","name":"_recipientAddress","type":"address"},{"internalType":"enum PayoutVault.RecipientStatus","name":"_status","type":"uint8"}],"name":"setRecipientStatus","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_newAddress","type":"address"}],"name":"setWorkerAddress","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes","name":"performData","type":"bytes"}],"name":"updateBalances","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"components":[{"internalType":"uint32","name":"campaignId","type":"uint32"},{"internalType":"uint256","name":"amount","type":"uint256"}],"internalType":"struct PayoutVault.RecipientEarnings[]","name":"_campaigns","type":"tuple[]"},{"internalType":"address","name":"_to","type":"address"}],"name":"withdrawManyRecipientEarnings","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint32","name":"_campaignId","type":"uint32"},{"internalType":"uint256","name":"_amount","type":"uint256"},{"internalType":"address","name":"_to","type":"address"}],"name":"withdrawRecipientEarnings","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint32","name":"_campaignId","type":"uint32"},{"internalType":"address","name":"_to","type":"address"}],"name":"withdrawRemainingCampaignBalance","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"workerAddress","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"}]
```


# Contact Spindl

Email us at contact\@spindl.xyz


# Start Here

Welcome! This guide will help you start running ads on your website or mobile app.

## 1. Onboarding

To get started, please reach out to the Spindl team to get your account approved as a Publisher. As part of that, you will need provide a Wallet Address for earnings to be sent to.

## 2. Set up a placement

Next, you'll need to set up at least one **Placement**. A placement is simply a place in your app where you show an ad - you can create as many as you need. When you create a Placement, you need to pick a **Creative Type** (full list [here](https://docs.google.com/spreadsheets/d/181QVJ3_HMJpHC2yk0F112DmguAHkzQCrLS0Ff9TKA74/edit?gid=0#gid=0)).

If you need a custom creative type for your placement, reach out to the Spindl team, and we can create it for you.

## 3. Technical Integration

There are three ways to integrate ads into your product. Most customers prefer the API approach for native integrations, and the React SDK for simple image-based ads.

{% tabs %}
{% tab title="API" %}
To use the API, you must first generate a Publisher API Token (you can find that on the [Settings](https://app.spindl.xyz/settings) screen).

{% hint style="warning" %}
This API Token should be treated as a secret, and not shared in any public-facing code.
{% endhint %}

Next, there are two APIs you will need to call.

1. **Fetch Recommendations**
   1. GET `https://e.spindlembed.com/v1/render/{publisher_id}`&#x20;
   2. Headers
      1. `X-API-ACCESS-KEY` : Publisher API Key, from the Settings tab
   3. Path Params
      1. `publisher_id`: Text, provided by the spindl team
   4. Required URL Params
      1. `placement_id`: Text, from the [Placements tab](https://app.spindl.xyz/placements)
      2. `address`: Text, the wallet address, in the form of 0x...
      3. `limit`: Numerical, the number of recommendations to return (usually this is 1)
      4. `country`: The country where the request originated from, should be a [2-letter country ](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2)code.
         1. (If you are using Cloudflare, you can pass in `CF-IPCountry` to this field directly)
   5. Optional URL Params
      1. `chain_id`: Numerical, the chain ID where this unit is being rendered
   6. Response:

```

{
  "items": [
    {
      "id": string,
      "impressionId": string,
      "type": string,  // specifies format of unit (card, iframe, discord)
      "title": string,
      "context": { // reason for returning unit (social context, onchain activity)
        "text": string
      },
      "description": string,
      "imageUrl": string,
      "imageAltText": string,
      "ctas": [
        {
          "title": string,
          "href": string
        }
      ]
    },
    ...
  ]
}
```

2. **Post Impressions** (record anytime a recommendation is shown on screen)
   1. POST `https://e.spindlembed.com/v1/external/track`&#x20;
   2. Headers
      1. `X-API-ACCESS-KEY` : Publisher API Key, from the Settings tab
   3. Body (JSON)
      1. `type`: Text, should be `impression`or `click`
      2. `impression_id`: Text, the `impressionId` from the API above
   4. Response: Status Code 200
      {% endtab %}

{% tab title="React SDK" %}
Spindl provides a SDK for React to make it simple to drop in ads throughout your site. For this, you will need your Publisher ID and a Placement ID, both available on the [Placements tab](https://app.spindl.xyz/placements).

1. Install the React SDK ([NPM Link](https://www.npmjs.com/package/@spindl-xyz/embed-react))

```bash
npm i @spindl-xyz/embed-react
```

2. Add to your site

```tsx
 <BannerEmbed
    publisherId="my_website" // required (get from Spindl team)
    placementId="article_sidebar" // required (get from Spindl team)
    style={{
        width: "970px",
        height: "250px",
    }} // recommended to add desired width/height
    address={"0xSpindl"} // optional, can also be a list of addresses (["0x123", "0x456"])
    properties={{
        chain_id: 8453,
    }} // optional, you can pass in JSON blob with contextual information (ie, category of article) for better targeting
/>
```

{% hint style="info" %}
If there are no ads available, this will show up empty.
{% endhint %}
{% endtab %}

{% tab title="iFrame" %}
Spindl provides a direct iFrame link, to make it simple to drop in ads on your Website or Mobile App. For this, you will need your Publisher ID and a Placement ID, both available on the [Placements tab](https://app.spindl.xyz/placements).

```html
<iframe
    width="800"
    height="400"
    src="https://e.spindlembed.com/v1/serve?publisher_id=my_website&placement_id=article_sidebar&address=0xSpindl"
></iframe>
```

{% endtab %}
{% endtabs %}


