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.