Working with a coding agent? Point it at earnesty.app/docs/integration/llms-full.txt — all public integration guides in one Markdown file, no key required.

Integrating Earnesty — three steps

Your users post about your product on X, YouTube or TikTok. Earnesty verifies the post and tells your backend to reward the user. You keep your own end-user reward ledger. Your organization separately funds an Earnesty wallet for processing fees.

The whole integration is one column, one route, one call, in that order. Both examples below are TypeScript; the shapes are plain HTTP and JSON, so they transcribe to anything.

For processing funding, unlimited registration and queued validation, see processing credits.


1. One column — somewhere for a reward to land

Most products meter a plan, not a balance: a monthly quota, a question allowance, seats on a tier. A reward from Earnesty is extra on top of that, so it needs a place to accumulate that your entitlement check already reads.

-- On whatever row your entitlement check reads: the account, the org, the user.
ALTER TABLE accounts ADD COLUMN earned_bonus integer NOT NULL DEFAULT 0;

-- One row per reward we ever sent you. The UNIQUE constraint is the idempotency.
CREATE TABLE earnesty_rewards (
  reward_reference text PRIMARY KEY,
  account_id      text NOT NULL,
  reward_type     text NOT NULL,
  amount          integer NOT NULL,
  -- What is left of THIS grant. Spend against it (oldest first), so a
  -- revocation can give back one grant's unspent credits without touching
  -- another's. A flat balance alone cannot tell them apart.
  remaining       integer NOT NULL,
  received_at     timestamptz NOT NULL DEFAULT now()
);

Then one line where you compute what a user may do:

const allowed = plan.monthlyQuota + account.earned_bonus;

If your product already has a credits balance, skip the column and credit into that. The earnesty_rewards table stays: it is what makes the route below safe to call twice.


2. One route — receive a reward, apply it once

We POST to a URL you choose, with a bearer token you choose. The body is four fields:

{
  "productUserId": "acct_123",
  "amount": 10,
  "rewardType": "credits",
  "rewardReference": "claim:1841…"
}

Insert the reference; if it was already there, do nothing. Return 2xx either way.

// Express / Hono / a Supabase edge function — the shape is the same.
export async function receiveReward(req: Request): Promise<Response> {
  if (req.headers.get('authorization') !== `Bearer ${process.env.EARNESTY_REWARD_KEY}`) {
    return new Response('unauthorized', { status: 401 });
  }
  const { productUserId, amount, rewardType, rewardReference, kind } = await req.json();

  if (rewardType !== 'credits') {
    // A reward you have not shipped support for. Refuse it; do not guess.
    return Response.json({ error: 'unknown_reward_type' }, { status: 400 });
  }
  if (kind !== 'post' && kind !== 'carry' && kind !== 'test') {
    // Not everything that arrives here is credit: `revocation` takes some
    // back, and a kind you do not know is a
    // future occasion, not a failure. Crediting without looking is how a
    // revocation gets applied with the sign inverted. The full branch —
    // including giving back one grant's remainder — is in reward-endpoint.md.
    return Response.json({ success: true, ignored: true });
  }

  // Test rewards use this same balance update.
  const inserted = await db.transaction(async (tx) => {
    const { rowCount } = await tx.query(
      `INSERT INTO earnesty_rewards (reward_reference, account_id, reward_type, amount, remaining)
       VALUES ($1, $2, $3, $4, $4) ON CONFLICT (reward_reference) DO NOTHING`,
      [rewardReference, productUserId, rewardType, amount],
    );
    if (rowCount === 1) {
      await tx.query(`UPDATE accounts SET earned_bonus = earned_bonus + $1 WHERE id = $2`, [
        amount,
        productUserId,
      ]);
    }
    return rowCount === 1;
  });

  return Response.json({ success: true, credited: inserted });
}

That is the whole handler. Full contract, including why a duplicate must be a 2xx and what we do when it is not: the reward endpoint contract.

Check it before a single post exists. Integration → Send test reward sends a real reward — one unit of your selected reward type, to a user id you type (use an account of your own) — and then sends it again with the same reference. You see both answers, and the account should be up by one, not two. The dashboard lists every real delivery afterwards with what your endpoint said about it.

Test rewards change the selected account’s actual spendable balance. Use an account of your own. Apply kind: "test" with the same balance and deduplication logic as ordinary rewards: sending one unit twice must increase its spendable balance by exactly one unit. Logging-only or separate-ledger behavior is an explicit customization, not this example’s default.


3. One call — enroll a user, show them their code

Call this from your backend for every user at signup, not as an optional step — it registers them in your earned tier. What comes back is what they need to post — their claim code — and a link to their own status page.

const res = await fetch('https://api.earnesty.app/v1/users', {
  method: 'POST',
  headers: { Authorization: `Bearer ${EARNESTY_API_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({ appId: EARNESTY_APP_ID, productUserId: account.id }),
});
const { code, statusPageUrl } = await res.json();
// Show `code` and embed or link to `statusPageUrl`. Registration is unlimited.

Idempotent: calling again for the same user returns the same code. The link lasts 30 days: store statusPageUrl and statusPageUrlExpiresAt on the account’s server-side record and never email it. Embed the hosted page with the embed helper, or open it from a link. Custom-rendered white-label UI is not currently offered. Full registration contract: enrolling users.

Where the link belongs: your own limit screen. The moment a user reaches a limit is one only you can see — we do not hold your end-user reward balance — and it is the right place for the offer: one line, in your words, linking to statusPageUrl from a fresh call. The recurring offer is available regardless of balance, subject to its cooldown, limits and funding. Your app decides when to surface it; Earnesty does not monitor your users’ balances. A low-credit screen is a useful placement, not an eligibility trigger. See enrolling users.


Going live

Three credentials

VariableWhere it comes fromWhat your backend does with it
EARNESTY_API_KEYEarnesty: Integration → Connect your app. Starts with esk_Sends it on every call to Earnesty
EARNESTY_SIGNING_SECRETEarnesty: Generate signing secret on the same page. Shown onceVerifies that a delivery came from Earnesty
EARNESTY_REWARD_KEYYou: any long random string, such as openssl rand -hex 32Checks the bearer token on deliveries. Paste it into Earnesty beside your endpoint URL

Keep all three in your platform’s secret manager, never in the browser, the repository or logs.

An install script for your platform

Every platform stores secrets differently, so we don’t ship a script. Ask your coding assistant to write one for yours from this description:

  • Ask for the API key and the signing secret with hidden input. Refuse an API key that doesn’t start with esk_. An empty answer keeps the stored value, so the script can be re-run after rotating one of them.
  • Generate the reward key on the first run and keep it on later runs.
  • Store the three values in the platform’s secret manager and expose them to the backend under the names above, then redeploy.
  • Print the reward endpoint URL and the reward key to paste into Earnesty. Never print the API key or the signing secret.
  • Optionally, keep the integration off behind a switch such as EARNESTY_ENABLED, so the credentials can be installed before the code that uses them is released.

The order

  1. Install the credentials and deploy.
  2. Save your endpoint URL and reward key in Integration, then Send test reward to an account of your own.
  3. Switch enrollment on. New accounts enroll at signup. Existing accounts enroll on their first visit to your rewards screen, or with a one-off backfill. Enrollment is idempotent and free, so a backfill can run in batches and be re-run safely.
  4. Enter your own account’s ID in Integration → Check an account, then make one real post from it. See testing your integration.
  5. Activate an offer. Until then, enrolled users can’t earn, so enrolling everyone early is harmless.

Nothing in this list breaks if steps 2 to 4 happen in a different order.


How this API changes

Fields are only ever added, never removed or retyped. Parse what you use and ignore what you don’t recognise — an unknown field in a payload or reply is a new feature, not an error. There is no version header; if a breaking change ever becomes necessary, it will arrive behind an explicit opt-in, never by the existing contract shifting under you.

What you never do

  • Mirror a balance. Your end-user reward balance stays in your system; the Earnesty processing wallet is separate.
  • Parse rewardReference. Its format is ours. Switch on rewardType.
  • Poll us. Every reward is pushed, and retried for you if your endpoint is down.

Why the boundary sits here: you own the user and the ledger that serves them; we own finding the post and proving it qualifies. Neither side needs the other’s database.

Recognizable account names

Enrollment accepts an optional displayName (for example, “Acme Design Team”) for owner reporting. Keep productUserId as the stable reward-recipient ID. Names can be updated through repeat enrollment without changing reward routing. The dashboard distinguishes the account name from the actual X author of a post. See the enrollment contract for limits, update and clear behavior.

Contextual earning opportunities

Use the posting-offer API and optional button to show what an account can earn right now, the recurring reward, a Drop or a Milestone, after a useful result or when access runs low. Your app chooses placement and presentation; the hosted page supplies instructions and history.