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.

Contextual posting offers

Show an earning opportunity where it is useful: after a user creates something worth sharing, as credits run low, or beside an action that needs more access. Your app decides placement. Earnesty lists what this account can earn right now: the recurring reward, open Drops and Milestones. The hosted earnesty.page does the rest: the text to post, where to post it, the post check and the account’s history.

JSON API

From your backend, call POST /v1/posting-offer with your organization API key:

POST /v1/posting-offer
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json

{"appId":"YOUR_APP_UUID","productUserId":"workspace_123"}

Resolve productUserId from your authenticated account or workspace membership. Never accept an arbitrary account ID from the browser. Register the account using POST /v1/users first; this read-only endpoint does not enroll or restore accounts.

{
  "eligible": true,
  "reason": null,
  "checkedAt": "2026-09-22T10:00:00.000Z",
  "nextEligibleAt": null,
  "offer": {
    "kind": "recurring",
    "reward": {"amount": 50, "key": "credits", "labelSingular": "credit", "labelPlural": "credits"},
    "suggestedLabel": "Post about this to earn 50 credits",
    "url": "https://your-app.earnesty.page/?t=SIGNED_TOKEN",
    "expiresAt": "2026-09-22T10:15:00.000Z"
  },
  "offers": [
    {
      "id": "recurring", "kind": "recurring", "name": "Every post", "description": null,
      "available": true, "reason": null,
      "reward": {"amount": 50, "key": "credits", "labelSingular": "credit", "labelPlural": "credits"},
      "bonusUpTo": 25, "repeat": {"kind": "every_n_days", "days": 7},
      "nextEligibleAt": null, "startsAt": null, "endsAt": null, "placesLeft": null,
      "suggestedLabel": "Post about this to earn 50 credits",
      "url": "https://your-app.earnesty.page/?t=SIGNED_TOKEN&offer=recurring"
    },
    {
      "id": "drop:4c1c…", "kind": "drop", "name": "Launch week", "description": "For posts about our launch week.",
      "available": true, "reason": null,
      "reward": {"amount": 100, "key": "credits", "labelSingular": "credit", "labelPlural": "credits"},
      "bonusUpTo": null, "repeat": {"kind": "once"},
      "nextEligibleAt": null, "startsAt": "2026-09-22T00:00:00Z", "endsAt": "2026-09-29T00:00:00Z",
      "placesLeft": null,
      "suggestedLabel": "Post for Launch week to earn 100 credits",
      "url": "https://your-app.earnesty.page/?t=SIGNED_TOKEN&offer=drop%3A4c1c…"
    }
  ],
  "expiresAt": "2026-09-22T10:15:00.000Z"
}

eligible, reason, nextEligibleAt and offer describe the recurring reward. offers lists every offer, recurring first, then Drops (open, and scheduled ones with reason: "not_started"), then Milestones. Each url opens the rewards page with that offer already chosen; it is present only while the offer is available. placesLeft is null for a Drop with no limit. A Drop or Milestone the account has already earned, or cannot earn now, comes with available: false and a reason (already_earned, not_started, funding_unavailable, unavailable).

Use your own button, banner or access screen. Render the reward’s labels, not a hard-coded currency. url opens the hosted rewards page, which reads current eligibility before showing the posting instructions and X composer. It does not post automatically.

Ineligible accounts receive HTTP 200 with eligible: false, offer: null and a reason: released, banned, not_enrolled, offer_inactive, lifetime_limit, funding_unavailable, cooldown or offer_unavailable. Only cooldown supplies a nextEligibleAt; that date does not guarantee funding or future eligibility. Hide the CTA, or explain the state in your own UI. A Drop or Milestone in offers can still be open while the recurring reward waits.

Eligibility is a snapshot, not a reserved reward. Terms, funding and account status can change. Verification still decides whether the published post qualifies and the amount actually granted, including any remaining lifetime allowance. Refresh when showing the opportunity and when the user returns to your app. Do not persist or log the response, put it in analytics, or share signed links between accounts. Responses are private/no-store; your backend should return the same cache headers. Links expire after 15 minutes (expiresAt; null when no offer is available and no link was issued).

Errors use { "error": "...", "code": "..." }: 400 invalid_json, invalid_app_id, invalid_product_user_id; 401 unauthorized; 404 app_not_found or user_not_found; 503 offer_unavailable. Treat errors as unavailable; do not invent an offer.

Optional button helper

Load /embed.js from your rewards-page origin. mountOffer uses the same JSON contract; no framework or browser API key is needed. Style the generated anchor with your own CSS.

<div id="earn-opportunity"></div>
<script src="https://your-app.earnesty.page/embed.js"></script>
<script>
  const button = Earnesty.mountOffer('#earn-opportunity', {
    className: 'your-button-class',
    getOffer: async () => {
      const response = await fetch('/api/my-posting-offer', { cache: 'no-store' });
      if (!response.ok) throw new Error('Offer unavailable');
      return response.json();
    },
    onError: () => { /* Optional retry UI; the CTA stays hidden. */ }
  });
  // button.refresh() when opening an access screen or after earning.
  // button.destroy() on logout, account change or route unmount.
</script>

Your authenticated /api/my-posting-offer backend calls Earnesty for the current account and returns the JSON. The button shows the recurring reward, or the first open Drop or Milestone while the recurring reward waits. Pass offerId (for example "drop:4c1c…", from offers) to show one offer only. The helper hides the link during loading, on failure, while nothing is available and after expiry. It refreshes on window focus and ignores stale requests. Destroy and remount when switching accounts. The full rewards-page embed remains available via Earnesty.mount; the contextual helper does not replace history or account management.