giggles api reference

view: reference | explorer (calls on the left, click one, try it)

the "aura market" HTTP API behind usegiggles.com, as observed from the outside. facts last verified 2026-09-23. this is not official documentation.

1. overview

Giggles is a social video app in which every post carries a pool of the in-app currency, aura. Users invest aura into posts, receive shares, and later redeem. The HTTP API described here is the one consumed by the official mobile clients; it has no published specification. Everything below was recorded by reading traffic and calling endpoints.

base URL and versioning

market APIhttps://api.usegiggles.com, paths under /api/v3 unless stated. Two profile routes live under /api/user, uploads under /api/uploads, and one wallet probe under /api/v2. auth backendA Supabase project (GoTrue for auth, PostgREST for direct messages). Written {SB} below, i.e. https://<project-ref>.supabase.co. The project ref is embedded in the official client and is not a secret. transportHTTPS; JSON request and response bodies; UTF-8. identifiersUsers and posts are UUIDs. A user's id is the Supabase sub claim. Written <user-uuid> and <post-uuid> below. timestampsISO 8601 in UTC where documented; one endpoint (trades) has been observed returning epoch values as well. amountsAura, as JSON numbers. Aura is not convertible to money (/api/v2/wallet reports hasWallet:false).

Variants of documented paths return 404. The following were probed and do not exist: /market/stats, /stats, /posts?limit, /posts/trending, /explore, /aura, /balance, /wallet (under v3), /api/v3/users/me, /chat/global.

headers

Every market-API request carries three headers.

headervaluenotes
AuthorizationBearer <access-token>A live Supabase access token. See authentication.
User-Agentokhttp/4.12.0The Android client's agent. The catalog records that "some endpoints are picky" about it; which ones is unverified. Sending it everywhere is the safe default.
Content-Typeapplication/jsonRequired on POST/PUT/PATCH bodies.

Supabase calls carry apikey: <publishable-key> in addition (see the publishable key).

Responses from the market API include x-ratelimit-remaining. See rate limits.

response conventions

  • Most list responses carry success: true and a payload key (posts, users, leaderboard, messages, transactions).
  • Paged lists use either an integer/string cursor with nextCursor + hasMore, or an integer offset with nextOffset + hasMore. Each entry states which.
  • Post objects returned by list endpoints are flat: market numbers appear as market_pot, market_volume, market_holders, market_trades, not as a nested market object.
  • Field casing is inconsistent across endpoints (aura_spendable vs realizedPnl). Names are given exactly as observed.
  • Errors are JSON. Shapes are collected in errors.

2. authentication

Identity is a Supabase GoTrue session. The market API accepts the session's access token as a bearer token and reads the sub claim as the caller's user id. There is no API key for the market API itself.

the publishable ("anon") key

Supabase projects expose a publishable key (historically the "anon" key). It identifies the project to GoTrue and PostgREST; it is compiled into the official client and is not a secret. It grants nothing by itself: PostgREST rows are filtered by row-level security against the user JWT, and GoTrue endpoints only issue tokens after a credential (OTP code, OAuth grant, refresh token) is presented.

callheaders
GoTrue (/auth/v1/*)apikey: <publishable-key>; on /otp and /token the client also sends Authorization: Bearer <publishable-key>; on /verify it sends apikey only. Both forms were accepted.
PostgREST (/rest/v1/*)apikey: <publishable-key> and Authorization: Bearer <access-token> (the user's JWT, never the publishable key, or RLS returns nothing).
market APINo apikey. Bearer only.

access tokens

A JWT signed by the project. Clients decode the payload (base64url, second segment) without verifying it; the platform is the authority.

claimtypemeaning
subuuidThe user id used in every /users/{id} path.
expinteger, epoch secondsExpiry. Observed lifetime ≈ 24 h (1438 minutes on a fresh token).
emailstringPresent for email and OAuth accounts. Required when updating the profile (PUT /api/user/profile).
phonestringPresent for phone accounts; used as phone_number in the profile update. inferred
app_metadata.providerstringgoogle, apple, or email.

An expired token yields 401 from the market API. There is no password grant for OAuth accounts; a session comes only from the flows below.

refresh tokens

Every session includes a refresh token: an opaque string, not a JWT. Exchanging it returns a new access token and a new refresh token (rotation). Store the returned pair; the client code in the gigglantir repository overwrites both on every exchange.

POST {SB}/auth/v1/token?grant_type=refresh_token
apikey: <publishable-key>
Authorization: Bearer <publishable-key>
Content-Type: application/json

{"refresh_token": "<refresh-token>"}
200 OK
{"access_token": "<access-token>", "refresh_token": "<refresh-token>", "expires_in": ..., "user": {...}}
  • Failure is a 4xx with {"error_description": "..."} (or msg). Common causes: the refresh token was already rotated, or the session was signed out.
  • Behaviour on reuse of an already-exchanged refresh token is unverified. GoTrue's documented default is to reject reuse outside a short grace window; treat a rejected exchange as "session lost, log in again".
  • When to refresh: the gigglantir keep-alive refreshes any token with ≤ 3 h left and treats a token with < 60 s left as absent. Refreshing early is safe; there is no observed cost.
  • Only the account's own refresh token can renew it. Tokens obtained by pasting a redirect URL without its refresh_token fragment cannot be renewed and must be re-issued by a fresh login.

email OTP

Two calls. No phone number is involved.

POST {SB}/auth/v1/otp
apikey: <publishable-key>
Authorization: Bearer <publishable-key>
Content-Type: application/json

{"email": "[email protected]", "create_user": false}
POST {SB}/auth/v1/verify
apikey: <publishable-key>
Content-Type: application/json

{"type": "email", "email": "[email protected]", "token": "123456"}
→ 200 {"access_token": "...", "refresh_token": "...", ...}
  • create_user: false restricts the code to existing accounts. Whether create_user: true produces an account with a provisioned username and wallet is unverified.
  • The project may require a captcha on /otp; the gigglantir connect page detects an error containing captcha and directs the user to OAuth instead. Whether this is on for all callers is unverified.
  • /verify works only if the project's email template sends a 6-digit code rather than a magic link. Error bodies use msg or error_description.

Google and Apple OAuth

GET {SB}/auth/v1/authorize?provider=google&redirect_to=<redirect-url>
→ 302 to the provider
→ 302 back to <redirect-url>#access_token=...&refresh_token=...&expires_in=...&token_type=bearer
  • provider is google or apple.
  • Tokens are returned in the URL fragment, so they never reach a server unless the page or the user forwards them. A script cannot complete this flow headlessly; a browser must.
  • Redirect allow-list: GoTrue only honours a redirect_to that the project has pre-registered; any other value falls back to the project's site URL. http://localhost:3000 is on the list (the gigglantir connect page relies on it: the browser lands on a dead localhost page and the user copies the address bar). The full list is unverified.
  • Native-app device attestation, if any, gates token issuance in the app, not this web flow: a JWT obtained here is a normal session.

keeping a session alive from a script

  1. Obtain one session by OTP (scriptable) or OAuth (browser once; paste the fragment).
  2. Persist access_token and refresh_token encrypted at rest. Never log either.
  3. Before each batch of calls, decode exp. If less than a few hours remain, exchange the refresh token and persist the new pair before using it.
  4. On 401 from the market API, exchange the refresh token once; if that fails, stop and require a new login. Do not loop.
  5. Run exactly one client per token. See sharing a token.
# decode exp without verifying (python)
import base64, json, time
def claims(tok):
    b = tok.split(".")[1]; b += "=" * (-len(b) % 4)
    return json.loads(base64.urlsafe_b64decode(b))
ttl_s = claims(ACCESS_TOKEN)["exp"] - time.time()

3. concepts

posts as pots

Every post is a pot of aura. There is no order book and no counterparty: a buy adds aura to the pot and mints shares; a redeem burns the caller's shares and pays out of the pot. The pot is the only thing that pays.

floor and level

The floor is a fixed seed amount that sets the curve's origin. It is not part of the pot and is not redeemable by holders. Native posts carry a floor of 1000. Imported posts (isImport: true, media imported from another platform) carry a smaller floor, reported as 100unverified; read floor from the market response rather than assuming it.

level = floor + pot

The level is the number the app displays as the post's price. A buy of I raises the level by exactly I; a redeem that pays R lowers it by exactly R.

shares and ownership

A buy of bet into a post with pot pot mints

shares = 1e9 · ln((floor + pot + bet) / (floor + pot))

and the resulting ownership fraction of the pool is

frac = bet / (floor + pot + bet)

A redeem pays frac × (floor + pot_now), i.e. the holder's fraction of the current level. Because the fraction is fixed at entry, every aura added later is worth frac of itself to an existing holder, and every aura removed later costs the same. Ownership is concave in size: a larger single buy purchases proportionally less of the pool. Observed density is roughly 140k–193k shares per aura, rising slightly with the pot.

maxInvest and the position cap

maxInvest is the largest single buy the post accepts from the caller right now. It implements a position cap: an account's position value may never exceed half the level.

maxInvest = level − 2 × (own position value)
  • Empty native post: level 1000, own 0 → maxInvest = 1000.
  • After a 1000 buy into that post: level 2000, own value 1000 → maxInvest = 0. A second buy from the same account returns 400 POSITION_CAP. The first entrant holds 50 % and cannot add.
  • A large stake can only sit on top of other people's aura: bet ≤ floor + others' pot.
  • maxInvest: 0 means "no further buy accepted", not "unknown". A buy above maxInvest is rejected whole; it is not clipped.

redeem is all-or-nothing

The redeem call sells the caller's entire position in a post. The amount field in the body is ignored (verified 2026-09-21: amount: 0.5 redeemed 100 % of shares). There are no partial exits.

exit impact and max loss

Because a redeem pays out of the pot, a holder's exit removes their fraction of the level from the pot. On posts with one dominant holder the pot typically falls by 98–99 % on that holder's exit. A full unwind by all holders pays out exactly the pot; sell order only redistributes it.

max loss on entry = I × pot / (floor + pot + I)

Entering an empty pot therefore has a maximum loss near zero; entering a large pot risks nearly the whole stake.

fees

feeratewhere it appears
creator fee≈ 0.1 % of a redemptioncreatorFee in the redeem response; accrues to the post's creator (creator_fees on market).
sell fee0.101 % flatDeducted from the redemption.
card-trade aura fee1 %limits.auraFeePct on trade-with.

sell locks and timers

The market response carries sell_locked_until and sell_timer_s (also seen as sellLockedUntil / sellTimerSeconds). They indicate a window during which a redeem is refused. When the lock is applied, and what a locked redeem returns, is unverified; a client should read the fields and not redeem before sell_locked_until.

deleted posts

A creator can delete a post. Afterwards market returns 404 and the post vanishes from lists, but redeem still works and refunds the position (reported as POST_GONE). The refund is normally near break-even; if the pot was drained before deletion the refund reflects the loss. postDeleted on portfolio positions flags this state.

feed embargo and fresh slots

The feed is not an index of live posts. Measured on two independent datasets (2026-09-16 and 2026-09-23):

  • Embargo: no post younger than ~15.1 minutes appears in the feed (hard edge at 900 s after created_at, then 10–100 s of jitter).
  • Fresh slots: newly listed posts occupy two fixed positions per page — around rank 3–7 and rank 37–48 — for roughly 1.5–3.5 minutes, then leave the feed. Nothing between 18 and 60 minutes old was observed.
  • The rest of a page is drawn from a pool of ~130–200 posts ordered mainly by pot size (rank vs pot Spearman −0.78). Roughly 200 distinct posts are ever surfaced.
  • Every request is a fresh random draw from that pool, regardless of cursor; nextCursor has always been 52. Paging does not enumerate the pool.
  • Trading is open before listing. A post is visible from insertion on its creator's posts list, and 40 % of posts receive a buy within 60 s of creation, before the feed shows them.
  • At the 15-minute mark every fresh post shows 0 likes, views, comments and shares; social counts cannot distinguish fresh posts.

the realtime tape

The app receives a websocket stream of trades from a Centrifugo server. The endpoint URL, channel names and subscription handshake are unverified here. What the stream carries, as consumed by a stdlib websocket client:

fieldtypemeaning
potAfternumberPot after the trade. The only authoritative pot source outside the market endpoint.
levelAfternumberLevel after the trade; floor = levelAfter − potAfter.
points, shares, side, postId, userId, usernameTrade identity. Sells carry no username.
  • Trades under ~10 aura are not published. A pot can move by several aura with no tape event.
  • The tape carries trades only. A new or empty post never appears on it; there is no post-created event.
  • The same trade can appear twice (once with pot 0→0, once populated). De-duplicate on (post, user, side, points, timestamp).
  • Replaying market/trades never reproduces the pot: it is capped at 100 rows and omits sub-10 trades.

4. endpoints

One entry per endpoint. "Auth: user" means a bearer access token of any account; "Auth: owner" means the token's own account is the subject. Response tables list the fields observed; unlisted fields may exist. Examples use placeholders.

feed

GET /api/v3/feed verified

One page of the ranked recommendation feed: ~50 posts with flat market fields and engagement counts.

Request

Auth: user. Standard headers.

Parameters

nameintypedescription
limitqueryintegerPage size. Clients use 50; larger values are unverified.
cursorqueryintegerValue of a previous nextCursor. Does not enumerate a stable list (see feed embargo).
sortquerystringtop is sent by the gigglantir wrapper for its top-videos ranking. Effect on ordering is inferred.

Response

fieldtypedescription
successboolean
postsarrayPost objects, flat (below).
nextCursorintegerObserved constant 52.
hasMoreboolean
degradedbooleanSet when the ranker fell back. Semantics unverified.

Post object (also returned by users/{id}/posts):

fieldtypedescription
iduuidPost id.
user_iduuidCreator.
usernamestringCreator's handle.
userobjectCreator summary; includes creatorverified.
post_descriptionstringCaption.
created_atISO 8601Creation time. Always ≥ ~15 min before the response (embargo).
hlsurlHLS manifest (master.m3u8).
media, carousel_media, video_thumbnailurl / array / urlMedia locations.
is_photoboolean
hashtagstring
source_platformstringOrigin for imported media. inferred
market_potnumberPot at ranking time.
market_volumenumberCumulative traded aura.
market_holdersinteger
market_tradesinteger
share_count, comment_count, bookmark_countintegerEngagement counts.
overallengagementscorenumberRanker score.
like_count, views, completionratePresent but not populated (always 0/absent) in feed responses.
friend_investors_countintegerinferred

Errors

  • 401 expired or missing token.
  • 429 budget exhausted.

Notes

  • Market numbers in a feed row are as of ranking; they were measured to be fresh (no post ever showed a pot older than the last trade in 8,386 checks).
  • Roughly 200 distinct posts are surfaced in total; deeper paging repeats.
  • Reading three pages every few seconds gives near-certain sight of every fresh slot; polling faster buys nothing.

Example

curl -s "https://api.usegiggles.com/api/v3/feed?limit=50" \
  -H "Authorization: Bearer <access-token>" \
  -H "User-Agent: okhttp/4.12.0"

posts

GET /api/v3/posts/<post-uuid>/market verified

Current market state of one post. The reference read before any buy.

Request

Auth: user. No body.

Parameters

nameintypedescription
idpathuuidPost id.

Response

fieldtypedescription
potnumber, auraRedeemable pool.
floornumber, auraSeed; 1000 on native posts.
levelnumber, aurafloor + pot.
maxInvestnumber, auraLargest buy the caller may place now (position cap). Caller-specific.
holdersintegerAccounts with a position.
tradesintegerTotal trade count, including trades the tape and /market/trades omit.
volumenumber, auraCumulative traded aura.
statusstringLifecycle state. Values unverified.
change24hnumber24-hour level change. Unit (aura vs percent) unverified.
creator_feesnumber, auraFees accrued to the creator. Also seen as creatorFeesEarned.
listed_atISO 8601Listing time. Also seen as listedAt.
isImportbooleanImported media. inferred
sell_locked_untilISO 8601 / nullSee sell locks. Also seen as sellLockedUntil.
sell_timer_sinteger / nullAlso seen as sellTimerSeconds.

Errors

  • 404 post deleted or unknown. A UUID that is a user, not a post, also 404s.
  • 401, 429.

Notes

  • pot < 1 with holders == 0 is the practical definition of an empty post. Posts a previous holder fully exited show pot ≈ 1.5, holders 0, trades ≥ 2.
  • maxInvest depends on the caller; the same post reports different values to different accounts.

Example

curl -s "https://api.usegiggles.com/api/v3/posts/<post-uuid>/market" \
  -H "Authorization: Bearer <access-token>" -H "User-Agent: okhttp/4.12.0"
# {"pot":0,"level":1000,"floor":1000,"maxInvest":1000,"holders":0,"trades":0,"volume":0,...}

GET /api/v3/posts/<post-uuid>/market/trades verified

The most recent trades on a post, newest first. Incomplete by design.

Request

Auth: user.

Parameters

nameintypedescription
idpathuuidPost id.
limitqueryintegerRows requested. Values above 100 clamp to 100.
offset, page, before, cursorqueryAccepted and ignored. There is no paging.

Response

fieldtypedescription
postIduuid
tradesarrayUp to 100 rows.
trades[].typestringbuy or sell.
trades[].pointsnumber, auraAura in (buy) or out (sell).
trades[].sharesnumber
trades[].userIduuid
trades[].usernamestringMay contain zero-width characters; strip U+200B before comparing.
trades[].photoUrlurl
trades[].atISO 8601 or epochBoth string and numeric (seconds or milliseconds) forms have been handled by clients; parse defensively.

Errors

  • 404 deleted post.
  • 401, 429.

Notes

  • Hard cap of 100 rows. A post with market.trades = 2883 returns 100 rows and no way to page.
  • Trades under ~10 aura are omitted (16 rows returned against trades = 156 on one post). Use market.trades for the true count.
  • Never derive the pot from this list; it cannot be reconstructed. Use market or the tape.
  • The list lags the pot by seconds under load.

Example

curl -s "https://api.usegiggles.com/api/v3/posts/<post-uuid>/market/trades?limit=100" \
  -H "Authorization: Bearer <access-token>" -H "User-Agent: okhttp/4.12.0"

POST /api/v3/posts/<post-uuid>/invest verified

Buy into a post. Adds points to the pot and mints shares to the caller.

Request

Auth: owner. Body: JSON.

Parameters

nameintypedescription
idpathuuidPost id.
pointsbodynumber, auraAmount to invest. Must be ≤ the caller's maxInvest and ≤ aura_spendable.

Response

fieldtypedescription
potnumber, auraPot after the buy. Equals points exactly when the pot was empty before.
sharesnumberShares minted to the caller.
othersFurther fields exist; clients read only the two above. inferred

Errors

status / codebodymeaning
400 POSITION_CAP{"code":"POSITION_CAP","maxPoints":N}points exceeds the cap. maxPoints is the amount that would be accepted; 0 means none.
400points above maxInvest or above spendable balance.
404Deleted post.
401, 429Token expired; budget exhausted (the buy did not happen).

Notes

  • A rejected buy is rejected whole. To buy the maximum, re-issue with points = maxPoints.
  • Round trip is ~0.8 s mean; 1–14 % of calls exceed 2 s depending on platform load.
  • Buys are accepted before a post is listed in the feed.

Example

curl -s -X POST "https://api.usegiggles.com/api/v3/posts/<post-uuid>/invest" \
  -H "Authorization: Bearer <access-token>" -H "User-Agent: okhttp/4.12.0" \
  -H "Content-Type: application/json" -d '{"points": 100}'

POST /api/v3/posts/<post-uuid>/redeem verified

Sell the caller's entire position in a post.

Request

Auth: owner. Body: JSON.

Parameters

nameintypedescription
idpathuuidPost id.
amountbodynumberIgnored. Clients send 1. Any value redeems 100 % of shares.

Response

fieldtypedescription
postIduuid
redeemednumber, auraAura paid out, net of fees.
sharesnumberShares burned.
realizedPnlnumber, auraredeemed − invested.
creatorFeenumber, auraFee paid to the creator (≈ 0.1 %).
potnumber, auraPot after the redeem.
levelnumber, auraLevel after the redeem.
counterMeaning unverified.
spendablenumber, auraCaller's liquid balance after the redeem.

Errors

  • POST_GONE: the post was deleted; the position is refunded anyway (see deleted posts). Exact status and body shape inferred.
  • 401, 429. A sell-lock refusal (see locks) has an unverified shape.
  • Redeeming with no position: unverified.

Notes

  • All-or-nothing. Two accounts redeeming the same post in sequence each receive their fraction of the level at the moment of their call; order does not change the group total.
  • Round trip ≈ 0.36 s in a burst of sequential redeems.

Example

curl -s -X POST "https://api.usegiggles.com/api/v3/posts/<post-uuid>/redeem" \
  -H "Authorization: Bearer <access-token>" -H "User-Agent: okhttp/4.12.0" \
  -H "Content-Type: application/json" -d '{"amount": 1}'

users

GET /api/v3/users/<user-uuid>/profile verified

Public profile of any user, including balances.

Request

Auth: user. Works for any id, including the caller's own.

Parameters

nameintypedescription
idpathuuidUser id (sub).

Response

fieldtypedescription
userobjectWrapper object; all fields below are inside it.
user.usernamestring
user.display_name, user.bio, user.college, user.profile_photostring
user.social_linksarray
user.auranumber, auraTotal net worth: spendable plus the value of open positions. Matches the leaderboard's networth. Stable under heavy trading.
user.aura_spendablenumber, auraLiquid balance. Holdings = aura − aura_spendable.
user.creatorverified, user.creatorverifiedgoldbooleanVerification badges.
user.followers_count, user.leaderboard_rankintegerinferred (read by the gigglantir connect page).
user.follower_count, user.following_count, user.activity_publicinteger / booleaninferred (read by the wrapper).
user.graduation_year, user.profile_colorPassed through by the profile updater. inferred

Errors

  • 404 unknown user (a post UUID also 404s here).
  • 401, 429.

Notes

  • Cheapest balance read: one call, two numbers.

Example

curl -s "https://api.usegiggles.com/api/v3/users/<user-uuid>/profile" \
  -H "Authorization: Bearer <access-token>" -H "User-Agent: okhttp/4.12.0"

GET /api/v3/users/<user-uuid>/stats verified

Trading statistics for any user.

Request

Auth: user.

Parameters

nameintypedescription
idpathuuidUser id.

Response

fieldtypedescription
netWorthnumber, aura
rankintegerLeaderboard rank.
totalVolumenumber, aura
tradeCountinteger
creatorVerifiedboolean
networth, realizedPnl, winRatenumberinferred from the wrapper's reader; may be absent.

Errors

  • 404, 401, 429.

Notes

Example

curl -s "https://api.usegiggles.com/api/v3/users/<user-uuid>/stats" \
  -H "Authorization: Bearer <access-token>" -H "User-Agent: okhttp/4.12.0"

GET /api/v3/users/<user-uuid>/posts verified

A creator's live posts with flat market fields, newest first.

Request

Auth: user.

Parameters

nameintypedescription
idpathuuidCreator's user id.

Response

fieldtypedescription
postsarrayFlat post objects: id, created_at, hashtag, is_photo, media, carousel_media, post_description, market_pot, market_volume, market_holders, market_trades. Same shape as feed rows; market (nested) is absent.

Errors

  • 404, 401, 429.

Notes

  • Live posts only. Deleted posts disappear.
  • A post appears here from the moment of insertion — roughly 15 minutes before the feed shows it. This is the earliest public surface for a new post, and the only one before listing.
  • No paging observed; the count returned for prolific creators is unverified.

Example

curl -s "https://api.usegiggles.com/api/v3/users/<user-uuid>/posts" \
  -H "Authorization: Bearer <access-token>" -H "User-Agent: okhttp/4.12.0"

GET /api/v3/users/<user-uuid>/transactions verified

A user's trade ledger, newest first. Readable for any user.

Request

Auth: user.

Parameters

nameintypedescription
idpathuuidUser id.
limitqueryintegerClients use 100.
cursorquerystringPaging cursor. inferred from the owner form below.

Response

fieldtypedescription
transactionsarrayLedger rows. (A bare array and a data key have also been tolerated by readers.)
transactions[].idstringStable row id; use for de-duplication.
transactions[].typestringbuy / sell (casing varies; compare case-insensitively).
transactions[].postIduuidAlso seen as post_id.
transactions[].pointsnumber, aura
transactions[].realizedPnlnumber, auraOn sells.
transactions[].createdAtISO 8601Also seen as created_at.

Errors

  • 404, 401, 429.

Notes

  • Any account's trading is public through this route; a user's own history is not private on this platform.

Example

curl -s "https://api.usegiggles.com/api/v3/users/<user-uuid>/transactions?limit=100" \
  -H "Authorization: Bearer <access-token>" -H "User-Agent: okhttp/4.12.0"

GET /api/v3/transactions verified

The caller's own full trade ledger. Same rows as the user-scoped route.

Request

Auth: owner.

Parameters

nameintypedescription
limitqueryintegerClients use 100.
cursorquerystringEmpty for the first page.

Response

As users/{id}/transactions.

Errors

  • 401, 429.

Notes

  • The cheapest source of an account's own history.

Example

curl -s "https://api.usegiggles.com/api/v3/transactions?limit=100&cursor=" \
  -H "Authorization: Bearer <access-token>" -H "User-Agent: okhttp/4.12.0"

GET /api/v3/users/<user-uuid>/portfolio verified

Open and closed positions of any user, 50 per page.

Request

Auth: user. Public for any id.

Parameters

nameintypedescription
idpathuuidUser id.
offsetqueryintegerFrom a previous nextOffset. Omit for page one.

Response

fieldtypedescription
positionsarrayOpen positions.
positions[].postIduuid
positions[].investednumber, auraCost basis.
positions[].valuenumber, auraMark: frac × level.
positions[].sharesnumber
positions[].unrealizedPnl, positions[].unrealizedPnlPct, positions[].realizedPnl, positions[].totalPnlnumber
positions[].creatorIduuidAlso recoverable from the thumbnail path /posts/thumbnails/<creator>/….
positions[].postDeletedbooleanPost deleted; redeem still refunds.
positions[].isImportboolean
positions[].description, positions[].thumbnailstring
closedPositionsarrayRows: openedAt, closedAt, invested, returned, realizedPnl, trades.
hasMoreboolean
nextOffsetinteger
currentobjectnetWorth, holdings, spendable. netWorth intermittently returns holdings only; holdings + spendable is reliable, and profile.aura is steadier still during rapid trading.
pnl24hobjectpnl, percentChange, rank.
totalPnl, totalPnlPercentnumberLifetime.

Errors

  • A non-200 for a real user indicates a private profile (inferred; status unverified).
  • 401, 429.

Notes

  • Page until hasMore is false or nextOffset repeats; clients cap at 6–30 pages.
  • value lags a fresh buy by a moment; spendable drops instantly. Net-worth computed as holdings + spendable can dip transiently.

Example

curl -s "https://api.usegiggles.com/api/v3/users/<user-uuid>/portfolio?offset=50" \
  -H "Authorization: Bearer <access-token>" -H "User-Agent: okhttp/4.12.0"

GET /api/v3/portfolio verified

The caller's own portfolio. Same shape and paging as the user-scoped route.

Request

Auth: owner.

Parameters

nameintypedescription
offsetqueryintegerFrom nextOffset.

Response

As users/{id}/portfolio.

Errors

  • 401, 429.

Notes

  • Exercised continuously by client code; not listed in the operator's catalog. Prefer this route for one's own account.

Example

curl -s "https://api.usegiggles.com/api/v3/portfolio" \
  -H "Authorization: Bearer <access-token>" -H "User-Agent: okhttp/4.12.0"

GET /api/v3/users/<user-uuid>/cards verified

Any user's card collection.

Request

Auth: user.

Parameters

nameintypedescription
idpathuuidUser id.

Response

Card objects; field names as in trade-with theirs.cards (inferred).

Errors

  • 404, 401, 429.

Notes

  • Read-only view; trade composition uses trade-with.

Example

curl -s "https://api.usegiggles.com/api/v3/users/<user-uuid>/cards" \
  -H "Authorization: Bearer <access-token>" -H "User-Agent: okhttp/4.12.0"

PUT /api/user/profile verified

Update the caller's profile. Full replacement: send every field.

Request

Auth: owner. Body: JSON. Not under /api/v3.

Parameters

nameintypedescription
username, display_name, bio, collegebodystringCurrent values, re-sent.
social_linksbodyarrayCurrent value; [] if none.
profile_photobodyurlPublic URL from presign. GIF allowed.
email or phone_numberbodystringRequired; taken from the JWT's email / phone claim.
graduation_year, profile_colorbodyOptional pass-through.

Response

200 with success: true or a body without error. inferred

Errors

  • POST /api/user/profile/create is onboarding-only and returns 409 for an existing profile. Do not use it for updates.
  • 401, 429.

Notes

  • Fetch profile first and pass every existing field through, or fields are wiped.

Example

curl -s -X PUT "https://api.usegiggles.com/api/user/profile" \
  -H "Authorization: Bearer <access-token>" -H "User-Agent: okhttp/4.12.0" \
  -H "Content-Type: application/json" \
  -d '{"username":"…","bio":"…","display_name":"…","social_links":[],"college":null,
       "profile_photo":"https://…/<key>","email":"[email protected]"}'

leaderboard

GET /api/v3/leaderboard verified

Net-worth ranking, 50 per page, with the caller's own row.

Request

Auth: user.

Parameters

nameintypedescription
periodquerystring24h is verified. 7d, 30d, all are forwarded by the wrapper; acceptance upstream is inferred. Default is the all-time ranking.
cursorquerystringFrom nextCursor. Walks stop at about page 120 (6,000 rows).

Response

fieldtypedescription
successboolean
periodstring
leaderboardarray50 rows.
leaderboard[].userIduuid
leaderboard[].username, leaderboard[].displayName, leaderboard[].photoUrlstring
leaderboard[].verified, leaderboard[].verifiedGoldboolean
leaderboard[].networthnumber, auraTotal wealth including open positions (equals profile.aura).
leaderboard[].rank, leaderboard[].delta, leaderboard[].percentChangenumberPresent on period rankings. inferred
meobjectThe caller's row (userId, username, …).
hasMore, nextCursorboolean / string

Errors

  • 401, 429 (a full walk on one token has hit 429).

Notes

  • Networth decays to 0 by roughly rank 3,000–6,000; the top few thousand rows are effectively the whole money supply.
  • Non-trade changes in networth are usually mark-to-market of open positions, not transfers.

Example

curl -s "https://api.usegiggles.com/api/v3/leaderboard?period=24h" \
  -H "Authorization: Bearer <access-token>" -H "User-Agent: okhttp/4.12.0"

chat and direct messages

GET /api/v3/chat verified

The global chat room.

Request

Auth: user.

Parameters

nameintypedescription
cursorquerystringFrom nextCursor. inferred

Response

fieldtypedescription
successboolean
chatterCountinteger≈ 13k observed.
messagesarray
messages[].id, messages[].kindstringkind: user observed.
messages[].authorUserId, messages[].username, messages[].avatarUrl, messages[].authorGold, messages[].clanAuthor.
messages[].text, messages[].gifUrl, messages[].replyToContent.
messages[].reactions, messages[].reactionCount, messages[].meta, messages[].gameId, messages[].createdAt
hasMore, nextCursor

Errors

  • 401, 429.

Notes

  • /chat/global does not exist; this route is the global room.

Example

curl -s "https://api.usegiggles.com/api/v3/chat" \
  -H "Authorization: Bearer <access-token>" -H "User-Agent: okhttp/4.12.0"

POST /api/v3/chat verified

Post a message to the global chat.

Request

Auth: owner. Body: JSON.

Parameters

nameintypedescription
textbodystringMessage text.

Response

{"success": true, "message": {…}} with the created message object.

Errors

  • 401, 429.

Notes

  • Not gated on creator verification; an unverified account posted successfully.

Example

curl -s -X POST "https://api.usegiggles.com/api/v3/chat" \
  -H "Authorization: Bearer <access-token>" -H "User-Agent: okhttp/4.12.0" \
  -H "Content-Type: application/json" -d '{"text": "hello"}'

Direct messages do not go through api.usegiggles.com. They are rows in the Supabase database, read and written over PostgREST at {SB}/rest/v1/ with apikey + the user's bearer token. Row-level security scopes every query to the caller: a user sees only chats they participate in and only their own chat_participants row. PostgREST filter syntax applies (eq., neq., in.(…), select=, order=, limit=).

GET {SB}/rest/v1/chat_participants verified

The caller's conversation list: one row per chat the caller belongs to.

Request

Auth: user JWT + apikey.

Parameters

nameintypedescription
user_idquery filteruuideq.<user-uuid> — the caller's own id (RLS returns nothing else).
is_hiddenquery filterbooleaneq.false to exclude hidden chats.
selectquerystringColumn list, e.g. chat_id,unread_count,last_read_at.

Response

columntypedescription
chat_iduuid
user_iduuid
joined_at, last_read_attimestamp
unread_countinteger
is_muted, is_pinned, is_hiddenboolean

Errors

  • 401 bad or expired JWT; an empty array when RLS excludes everything.

Notes

  • The other party of a chat is not on this row. Resolve it from chat_messages (a sender ≠ self) or from chats.last_message_by. For an empty chat the other party is unknowable.

Example

curl -s "{SB}/rest/v1/chat_participants?user_id=eq.<user-uuid>&is_hidden=eq.false&select=chat_id,unread_count,last_read_at&limit=200" \
  -H "apikey: <publishable-key>" -H "Authorization: Bearer <access-token>"

GET {SB}/rest/v1/chats verified

Chat headers: last message preview and timestamps.

Request

Auth: user JWT + apikey.

Parameters

nameintypedescription
idquery filteruuid listin.(a,b,c); clients batch 40 ids per call.
selectquerystringe.g. id,last_message,last_message_time,last_message_by.

Response

columntypedescription
iduuid
created_at, last_message_timetimestamp
last_messagestringPreview text.
last_message_byuuid
last_message_iduuid

Errors

  • 401.

Notes

  • A sending client updates this row itself after inserting a message (PATCH chats?id=eq.<chat-uuid> with the four last_message* columns, Prefer: return=minimal); the server does not maintain it.

Example

curl -s "{SB}/rest/v1/chats?id=in.(<chat-uuid>,<chat-uuid>)&select=id,last_message,last_message_time,last_message_by" \
  -H "apikey: <publishable-key>" -H "Authorization: Bearer <access-token>"

GET {SB}/rest/v1/chat_messages verified

Messages in a chat.

Request

Auth: user JWT + apikey.

Parameters

nameintypedescription
chat_idquery filteruuideq.<chat-uuid> or in.(…).
user_idquery filteruuidneq.<user-uuid> to find the other party.
orderquerystringcreated_at.desc.
limitqueryinteger

Response

columntypedescription
id, chat_id, user_iduuid
textstringA clan invite is the text clan_invite:<id> plus a row in clan_invites.
message_typeenumtext, media, post, gift, market_event, system.
media, post_id, reply_to_message_id, reactions, meta, read_by
client_iduuidClient-generated idempotency id.
created_attimestamp

Errors

  • 401.

Notes

  • Mark as read by patching the caller's participant row (below).

Example

curl -s "{SB}/rest/v1/chat_messages?chat_id=eq.<chat-uuid>&select=id,user_id,text,message_type,media,post_id,meta,created_at,reply_to_message_id&order=created_at.desc&limit=100" \
  -H "apikey: <publishable-key>" -H "Authorization: Bearer <access-token>"

POST {SB}/rest/v1/chat_messages verified

Send a direct message.

Request

Auth: user JWT + apikey. Headers: Content-Type: application/json, Prefer: return=representation to receive the inserted row.

Parameters

nameintypedescription
chat_idbodyuuidExisting chat the caller belongs to.
user_idbodyuuidThe caller's own id.
textbodystring
message_typebodystringtext.
client_idbodyuuidFresh UUID v4.
metabodyobjectOptional; e.g. {"trade_offer_id": …} to attach a card offer.

Response

201 with a one-element array containing the row (under return=representation).

Errors

  • 401; 403/4xx from RLS when the caller is not a participant.

Notes

  • Creating a new chat (rows in chats and two chat_participants) is unverified; only sending into existing chats has been exercised.
  • After the insert, patch chats so the conversation list preview updates (see chats).

Example

curl -s -X POST "{SB}/rest/v1/chat_messages" \
  -H "apikey: <publishable-key>" -H "Authorization: Bearer <access-token>" \
  -H "Content-Type: application/json" -H "Prefer: return=representation" \
  -d '{"chat_id":"<chat-uuid>","user_id":"<user-uuid>","text":"hi","message_type":"text","client_id":"<uuid4>"}'

PATCH {SB}/rest/v1/chat_participants verified

Mark a chat read, or hide it. Only the caller's own row is writable.

Request

Auth: user JWT + apikey. Prefer: return=minimal.

Parameters

nameintypedescription
chat_id, user_idquery filteruuideq. both, or chat_id=in.(…) for bulk.
last_read_atbodytimestampMark read: now, with unread_count: 0.
is_hiddenbodybooleantrue hides the chat from the caller's list. There is no delete.

Response

204 (or 200) with no body.

Errors

  • 401. A filter matching another user's row updates nothing and returns success.

Notes

  • Hiding is per participant; the other party's view is unchanged.

Example

curl -s -X PATCH "{SB}/rest/v1/chat_participants?chat_id=eq.<chat-uuid>&user_id=eq.<user-uuid>" \
  -H "apikey: <publishable-key>" -H "Authorization: Bearer <access-token>" \
  -H "Content-Type: application/json" -H "Prefer: return=minimal" \
  -d '{"is_hidden": true}'

cards, offers and gifts

Cards are a parallel collectible economy that shares the aura wallet. Trades are offers between two users containing cards and/or aura on each side. Because an offer must contain at least one card, an aura gift is expressed as an offer of one card plus N aura against 1 aura.

GET /api/v3/cards/trade-with/<user-uuid> verified

Everything needed to compose an offer to one user: both inventories, spendable aura, limits.

Request

Auth: owner.

Parameters

nameintypedescription
idpathuuidCounterparty user id.

Response

fieldtypedescription
otherobjectCounterparty summary.
mine.cards, mine.cosmeticsarrayCaller's tradeable items.
mine.spendableAuranumber, aura
theirs.cardsarrayCounterparty's cards.
cards[].cardIdstringCatalog id.
cards[].userCardIduuidThe instance id used in offers.
cards[].name, cards[].tier, cards[].editionNo, cards[].imageUrl, cards[].animationUrl
cards[].instaSellAuranumber, auraInstant-sell value.
cards[].tradeable, cards[].lockedReasonboolean / stringReliable here (not in inventory).
limits.maxItemsPerSideinteger6.
limits.openOutgoingMaxinteger1000.
limits.offersToThisUserMaxPerDayinteger25.
limits.auraFeePctnumber1.

Errors

  • 404 unknown user; 401, 429.

Notes

  • The authoritative composer; use it instead of /cards/inventory before creating an offer.

Example

curl -s "https://api.usegiggles.com/api/v3/cards/trade-with/<user-uuid>" \
  -H "Authorization: Bearer <access-token>" -H "User-Agent: okhttp/4.12.0"

POST /api/v3/cards/trades verified

Create a trade offer.

Request

Auth: owner. Body: JSON.

Parameters

nameintypedescription
receiverIdbodyuuidCounterparty.
offeredItemsbodyarray[{"itemType":"card","id":"<userCardId>"}]. itemType is lowercase card (also sticker); id holds the userCardId.
requestedItemsbodyarraySame shape, from theirs.cards.
offeredAurabodynumber
requestedAurabodynumber

Response

fieldtypedescription
okboolean
offerIduuid
expiresAttimestamp≈ 7 days.

Errors

status / codebodymeaning
400 empty_sideA side has no items and 0 aura.
400 no_card_in_tradeNeither side contains a card. Pure-aura transfers are refused.
400 Invalid itemsWrong item key or id. Checked after the two above.
401, 429

Notes

  • Validation order is empty_sideno_card_in_tradeInvalid items.
  • Aura gift: offer [one card] + N aura, request 1 aura. The recipient nets ≈ N minus the 1 % fee plus a card.
  • Gift counts and cooldowns are not exposed.

Example

curl -s -X POST "https://api.usegiggles.com/api/v3/cards/trades" \
  -H "Authorization: Bearer <access-token>" -H "User-Agent: okhttp/4.12.0" \
  -H "Content-Type: application/json" \
  -d '{"receiverId":"<user-uuid>","offeredItems":[{"itemType":"card","id":"<userCardId>"}],
       "requestedItems":[],"offeredAura":100,"requestedAura":1}'

POST /api/v3/cards/trades/<offer-uuid>/{accept|decline|cancel} verified

Resolve an offer. accept and decline by the receiver; cancel by the sender.

Request

Auth: owner. No body.

Parameters

nameintypedescription
offerIdpathuuidFrom offerId on creation or from the list.
actionpathenumaccept, decline, cancel.

Response

{"ok": true, …}. inferred

Errors

  • 4xx when the caller is the wrong party or the offer expired. Shapes unverified.

Notes

  • Accepting settles both sides atomically, including aura, less the 1 % fee.

Example

curl -s -X POST "https://api.usegiggles.com/api/v3/cards/trades/<offer-uuid>/accept" \
  -H "Authorization: Bearer <access-token>" -H "User-Agent: okhttp/4.12.0"

GET /api/v3/cards/trades verified

The caller's offers by status.

Request

Auth: owner.

Parameters

nameintypedescription
statusqueryenumpending, accepted, incoming.

Response

{"offers": [ … ]}; offer fields inferred.

Errors

  • 401, 429.

Notes

  • incoming lists offers made to the caller.

Example

curl -s "https://api.usegiggles.com/api/v3/cards/trades?status=incoming" \
  -H "Authorization: Bearer <access-token>" -H "User-Agent: okhttp/4.12.0"

GET /api/v3/cards/inventory inferred

The caller's own cards.

Request

Auth: owner.

Parameters

None.

Response

Card objects. The tradeable flag here is unreliable; use trade-with.

Errors

  • 401, 429.

Notes

  • Listed in the operator's catalog without a live mark.

Example

curl -s "https://api.usegiggles.com/api/v3/cards/inventory" \
  -H "Authorization: Bearer <access-token>" -H "User-Agent: okhttp/4.12.0"

GET /api/v3/cards/catalog inferred

The card catalog: 57 cards across 6 tiers (including "contraband").

Request

Auth: user.

Parameters

None.

Response

Catalog entries; field names unverified.

Errors

  • 401, 429.

Notes

  • Counts as of 2026-09-20.

Example

curl -s "https://api.usegiggles.com/api/v3/cards/catalog" \
  -H "Authorization: Bearer <access-token>" -H "User-Agent: okhttp/4.12.0"

GET /api/v3/cards/boxes inferred

Loot-box state for the caller.

Request

Auth: owner.

Parameters

None.

Response

fieldtypedescription
spendablenumber, aura
keyBalanceinteger
pendingBoxesarray

Errors

  • 401, 429.

Notes

  • Box prices: 1★ = 50 aura … 5★ = 2500 aura. The purchase/open route is unverified.

Example

curl -s "https://api.usegiggles.com/api/v3/cards/boxes" \
  -H "Authorization: Bearer <access-token>" -H "User-Agent: okhttp/4.12.0"

GET /api/v3/cards/market/auctions · /api/v3/cards/market/lots inferred

The card marketplace: auctions and fixed-price lots.

Request

Auth: user.

Parameters

unverified.

Response

unverified.

Errors

  • 401, 429.

Notes

  • Listed by the operator's catalog; never exercised by client code.

Example

curl -s "https://api.usegiggles.com/api/v3/cards/market/lots" \
  -H "Authorization: Bearer <access-token>" -H "User-Agent: okhttp/4.12.0"

GET /api/v3/rewards/daily inferred

Daily reward state. Resets at 16:00 UTC.

Request

Auth: owner.

Parameters

None.

Response

unverified.

Errors

  • 401, 429.

Notes

  • Whether a claim is a separate POST is unverified.

Example

curl -s "https://api.usegiggles.com/api/v3/rewards/daily" \
  -H "Authorization: Bearer <access-token>" -H "User-Agent: okhttp/4.12.0"

profile photo and media uploads

Uploads are three steps: presign, PUT the bytes to object storage, then reference the public URL from the profile (or a post). The flow was recovered from the client bundle and is exercised end-to-end by the gigglantir profile-photo tool.

POST /api/uploads/presign verified

Obtain a presigned PUT URL for an image.

Request

Auth: owner. Body: JSON. Not under /api/v3.

Parameters

nameintypedescription
kindbodyenumprofile-image or post-media.
contentTypebodystringimage/jpeg, image/png, image/gif, image/webp.
contentLengthbodyintegerExact byte length of the upload.

Response

fieldtypedescription
uploadUrlurlPresigned PUT (Cloudflare R2).
keystringObject key.
publicUrlurlThe URL to store on the profile.
expiresIninteger, seconds300.
requiredHeadersobjectHeaders the PUT must carry (at least Content-Type).

Errors

  • 4xx for an unsupported type or size; shape unverified. 401.

Notes

  • The gigglantir tool caps uploads at 8 MB client-side; the platform's limit is unverified.

Example

curl -s -X POST "https://api.usegiggles.com/api/uploads/presign" \
  -H "Authorization: Bearer <access-token>" -H "User-Agent: okhttp/4.12.0" \
  -H "Content-Type: application/json" \
  -d '{"kind":"profile-image","contentType":"image/png","contentLength":12345}'

PUT {uploadUrl} verified

Upload the raw bytes to the presigned URL.

Request

No bearer token: the URL is self-authorising. Headers: exactly requiredHeaders from presign. Body: the file bytes.

Parameters

None.

Response

200, 201 or 204, empty body.

Errors

  • 403 after expiresIn or on a header/length mismatch.

Notes

Example

curl -s -X PUT "<uploadUrl>" -H "Content-Type: image/png" --data-binary @avatar.png

auth (Supabase GoTrue)

POST {SB}/auth/v1/otp verified

Email a one-time code (or magic link) to an address.

Request

Headers: apikey, Authorization: Bearer <publishable-key>, Content-Type: application/json.

Parameters

nameintypedescription
emailbodystring
create_userbodybooleanfalse: existing accounts only. true: unverified whether the resulting account is provisioned.

Response

200, empty object.

Errors

  • 4xx with msg / error_description / error; a message containing captcha means the project demands a captcha token this call did not supply.

Notes

  • Rate-limited by GoTrue per address; limits unverified.

Example

curl -s -X POST "{SB}/auth/v1/otp" \
  -H "apikey: <publishable-key>" -H "Authorization: Bearer <publishable-key>" \
  -H "Content-Type: application/json" -d '{"email":"[email protected]","create_user":false}'

POST {SB}/auth/v1/verify verified

Exchange the emailed code for a session.

Request

Headers: apikey, Content-Type: application/json.

Parameters

nameintypedescription
typebodystringemail.
emailbodystring
tokenbodystringThe 6-digit code.

Response

fieldtypedescription
access_tokenJWT
refresh_tokenstring
expires_in, token_type, userStandard GoTrue session fields. inferred

Errors

  • 4xx with msg / error_description: invalid or expired code.

Notes

  • Codes are single-use and short-lived.

Example

curl -s -X POST "{SB}/auth/v1/verify" \
  -H "apikey: <publishable-key>" -H "Content-Type: application/json" \
  -d '{"type":"email","email":"[email protected]","token":"123456"}'

GET {SB}/auth/v1/authorize verified

Start an OAuth login in a browser.

Request

Browser navigation; no headers.

Parameters

nameintypedescription
providerqueryenumgoogle, apple.
redirect_toqueryurlMust be on the project's allow-list.

Response

302 chain ending at redirect_to#access_token=…&refresh_token=….

Errors

  • A non-listed redirect_to lands on the project's default site URL instead.

Notes

  • The fragment is not sent to the redirect host; read it from the address bar or page script.

Example

open "{SB}/auth/v1/authorize?provider=google&redirect_to=http%3A%2F%2Flocalhost%3A3000"

POST {SB}/auth/v1/token?grant_type=refresh_token verified

Rotate a session.

Request

Headers: apikey, Authorization: Bearer <publishable-key>, Content-Type: application/json.

Parameters

nameintypedescription
grant_typequerystringrefresh_token.
refresh_tokenbodystringThe current refresh token.

Response

access_token, refresh_token (new), expires_in, user. See refresh tokens.

Errors

  • 4xx with error_description: token already used, revoked, or malformed.

Notes

  • Persist the new pair before using it. Keep one writer per account.

Example

curl -s -X POST "{SB}/auth/v1/token?grant_type=refresh_token" \
  -H "apikey: <publishable-key>" -H "Authorization: Bearer <publishable-key>" \
  -H "Content-Type: application/json" -d '{"refresh_token":"<refresh-token>"}'

wallet and realtime

GET /api/v2/wallet inferred

Wallet capability probe.

Request

Auth: owner.

Parameters

None.

Response

Includes hasWallet: false: aura is not backed by a payment wallet and is not withdrawable.

Errors

  • 401.

Notes

  • The only v2 route referenced. /api/v3/wallet and /api/v3/balance do not exist.

Example

curl -s "https://api.usegiggles.com/api/v2/wallet" \
  -H "Authorization: Bearer <access-token>" -H "User-Agent: okhttp/4.12.0"

WS realtime trade tape inferred

Push stream of trades over Centrifugo.

Request

Websocket. Endpoint, token exchange and channel names are unverified in the sources for this page.

Parameters

Response

Per-trade events with potAfter and levelAfter; see the realtime tape.

Errors

Notes

  • ~10-aura publish floor; sells lack username; duplicates occur.
  • No post-creation events. Empty posts are invisible on the stream.

Example

5. rate limits and etiquette

per-account budget

  • Limits are enforced per account (per token), not per IP or per client. Every request on a token draws from the same budget.
  • The budget is reported on responses in x-ratelimit-remaining; clients that do not see the header assume 100. Tooling refers to "100 per token". The window over which the 100 replenishes is unverified; no repository source documents it.
  • GoTrue has separate, unrelated limits on OTP sends.
  • A Retry-After header on 429 has not been observed or recorded. unverified

429 behaviour and backoff

  • A 429 means the request was not performed. For a buy or redeem, the balance is unchanged; do not assume a fill.
  • Read x-ratelimit-remaining on every response and stop discretionary reads before it reaches zero. Example policy: pause polling below 20 remaining and resume when a later response shows headroom.
  • On 429, sleep at least a few seconds before the next request on that token; back off exponentially on repeats. The client code in the gigglantir repository marks the account rate-limited and skips the whole cycle rather than retrying immediately.
  • Time-critical calls (buy, redeem) are the ones to protect: spend the budget on them, and route public reads (market, profile, leaderboard) through a cache or a separate read-only account.

why sharing a token between two clients breaks both

Two clients on one token share one budget with no partitioning: whichever request lands first consumes the slot. Each client then sees 429s it did not cause, at moments it cannot predict. For a client whose value depends on landing a request inside a short window, a throttle at the wrong moment does not cost a proportional share of outcomes; it costs the specific contested windows, which are the valuable ones. Run one client per token; if two processes need the platform, log in twice and give each its own session.

Automation on an account is visible from the outside: the account's transaction history is public, and dozens of trades per hour at a median spacing of a few seconds is an unambiguous signature.

etiquette

  • Send the client's User-Agent; do not spoof other identities.
  • Cache public reads. A market read is stale after ~10 s, a profile after ~45 s; nothing is gained by polling faster.
  • Do not page the leaderboard or feed continuously; the feed repeats after ~200 posts and the leaderboard walk has triggered 429s.
  • Never log or transmit tokens. Never share a token with a service you do not run.

6. the gigglantir wrapper (/api/v1)

A read-only, CORS-open JSON wrapper served at https://earlybirdgetstheaura.com/api/v1. It reshapes upstream responses into stable field names and adds a few datasets of its own. GET /api/v1 (or /api/v1/health) returns the index as JSON, no auth.

two families

familyauthdata sourcecaching
tokenAuthorization: Bearer <access-token> — the caller's own Giggles JWT (?token= works for a quick test). Validated locally (three segments, unexpired exp), then forwarded unchanged. Never stored or logged.api.usegiggles.comShared across callers by path: market 12 s, search 30 s, portfolio 30 s, profile/stats/posts 45 s, leaderboard 45 s, other 20 s. N callers asking the same public question cost one upstream call.
publicnoneThe wrapper's own database: the aurachain ledger, live-pool round totals, post trajectories, market-pulse samples10 s

Rules: GET only; allow-listed paths; ids must be UUIDs (else 400); no path can move aura or read a specific private account; the token family spends the caller's rate budget.

envelope and errors

Every response is an object with ok. Failures: {"ok": false, "error": "<message>"}, plus upstreamStatus when an upstream call failed.

statusmeaning
400Bad parameter (unknown period/by, q too short, non-UUID id, non-integer limit).
401Missing, malformed or expired bearer token (message says which).
403Upstream rejected the token (passed through).
404No such endpoint; see GET /api/v1.
429Upstream rate limit (passed through; it is the caller's budget).
502Upstream unreachable or returned an unexpected status.
503/public/pulse on a host with no sampler data.

token endpoints

endpointparamsreturns
GET /api/v1/leaderboardperiod = 24h (default) | 7d | 30d | all{ok, period, count, leaderboard[{rank, userId, username, displayName, networth, delta, percentChange, verified}]}
GET /api/v1/top-videosby = volume (default) | pot | holders | trades | shares | comments | bookmarks | engagement; limit 1–50 (default 20){ok, rankedBy, count, videos[{rank, …post}]}. Ranks one page of the top feed (≤ 50 posts), not history. Aliases /top_videos, /top.
GET /api/v1/searchq ≥ 2 chars{ok, query, count, members[{id, username, displayName, bio, photo, verified, hasWallet}]}. Aliases /members, /member-search.
GET /api/v1/feedlimit 1–50; cursor from nextCursor{ok, count, nextCursor, posts[…post]} — one feed page in the app's own order.
GET /api/v1/posts/<uuid>/market{ok, post, market{pot, floor, level, maxInvest, holders, trades, volume, status, change24h, creatorFees, listedAt, isImport, sellLockedUntil, sellTimerSeconds}}
GET /api/v1/posts/<uuid>/tradeslimit 1–100{ok, post, count, note, trades[{at, type, userId, username, points, shares}]}. Upstream cap of 100 and the sub-10 omission apply.
GET /api/v1/users/<uuid>/profile{ok, user{id, username, displayName, bio, photo, aura, auraSpendable, verified, followers, following, activityPublic}}
GET /api/v1/users/<uuid>/stats{ok, user, stats{rank, tradeCount, networth, realizedPnl, winRate}}
GET /api/v1/users/<uuid>/posts{ok, user, count, posts[…post]} — the creator's live posts, newest first; the earliest public surface for a new post.

The shared post shape: {id, userId, username, description, media, thumbnail, createdAt, market{pot, volume, holders, trades}, engagement{shares, comments, bookmarks, score}}.

public endpoints

endpointparamsreturns
GET /api/v1/public/chainlimit 1–200; before, after (seq); user (pseudonym){ok, rows[…]} — the aurachain: a hash-chained, append-only ledger of closed trades, fully pseudonymised.
GET /api/v1/public/chain/statsLedger totals, win rate, head hash.
GET /api/v1/public/chain/verify{ok, checked, head} or {ok:false, broken_at} — recomputes every hash from genesis.
GET /api/v1/public/chain/usersq (pseudonym prefix), limitPseudonymised accounts on the chain.
GET /api/v1/public/live/historylimit 1–500 (default 50){ok, count, rounds[{round, members, invested, returned, pnl, pnlPct, enteredAt, exitedAt, minutes}]} — closed live-pool rounds, totals only: no post ids, no names.
GET /api/v1/public/posts/<uuid>/trajectory{ok, post, count, intervalSeconds: 30, minAgeSeconds: 3600, points[{ts, pot, volume, holders, trades}]} — logged snapshots; rows younger than 1 h are never served; ≤ 2000 rows.
GET /api/v1/public/pulsehours 1–336 (default 24); keys comma list{ok, hours, since, series{key: [[ts, value], …]}}.
GET /api/v1/public/pulse/keys{ok, count, keys[]}.
GET /api/v1/public/stats{ok, ledger{…}}.
GET /api/v1/public/datasetPointer to the CC-BY-4.0 giggles-market dataset: {ok, name, license, tables[], note, url}.

examples

curl -s https://earlybirdgetstheaura.com/api/v1 | jq .endpoints
curl -s "https://earlybirdgetstheaura.com/api/v1/public/live/history?limit=5" | jq .
curl -s "https://earlybirdgetstheaura.com/api/v1/public/chain/verify" | jq .
curl -s -H "Authorization: Bearer <access-token>" \
  "https://earlybirdgetstheaura.com/api/v1/posts/<post-uuid>/market" | jq .market
curl -s -H "Authorization: Bearer <access-token>" \
  "https://earlybirdgetstheaura.com/api/v1/users/<user-uuid>/posts" | jq '.posts[0]'
// browser (CORS is open)
const r = await fetch("https://earlybirdgetstheaura.com/api/v1/leaderboard?period=7d",
  { headers: { Authorization: "Bearer " + ACCESS_TOKEN } });
const { ok, leaderboard } = await r.json();

7. errors

Status codes and bodies observed from the market API and the auth backend. Bodies are JSON unless stated.

status / codewherebodymeaning and handling
400 POSITION_CAPinvest{"code":"POSITION_CAP","maxPoints":N}Buy exceeds the 50 % cap. Retry with points ≤ maxPoints; 0 means the account cannot add to this post.
400investpoints above maxInvest or above spendable balance.
400 empty_sidecards/tradesAn offer side has no items and no aura.
400 no_card_in_tradecards/tradesNo card on either side. Aura-only transfers are refused.
400 Invalid itemscards/tradesWrong item shape; use {"itemType":"card","id":"<userCardId>"}.
401anyAccess token expired, malformed or revoked. Refresh once, then re-login.
403anyToken valid but rejected for this resource. Rare; passed through by the wrapper.
404market, trades, profile, unknown pathsDeleted post, unknown user, or a path that does not exist. A deleted post still redeems.
409POST /api/user/profile/createProfile already exists; use PUT /api/user/profile.
429anyPer-account budget exhausted. The request did not happen. Back off; see 429 behaviour.
POST_GONEredeemmarker in the redeem responsePost deleted; the position was refunded. Shape inferred.
4xx (GoTrue)/auth/v1/*{"msg": …} or {"error": …, "error_description": …}Invalid/expired code, unknown email, captcha required, refresh token rejected. Read msg then error_description.
401 (PostgREST)/rest/v1/*Missing apikey or bad JWT. An empty array, not an error, is what RLS returns for rows the caller may not see.
5xxanyoften non-JSONPlatform fault. Responses have contained control characters; strip \x00–\x1f before JSON parsing.

8. changelog and verification

This page was assembled on 2026-09-23 from the operator's catalog (consolidated 2026-09-20), client code that exercises the endpoints daily, and three measurement reports. Dates below say when each class of fact was last confirmed.

datefactstatus
2026-09-23Feed embargo (~15.1 min), two fresh slots per page, random re-draw per request, nextCursor constant 52; posts visible on the creator's list from insertion.verified on two datasets
2026-09-23Invest round trip 0.76 s mean; sequential redeems 0.36 s each; buys are rejected whole above maxInvest.verified
2026-09-22Share formula, ownership fraction, payout proportional to level, 50 % cap and maxInvest = level − 2×own, 400 POSITION_CAP with maxPoints.verified on the tape and by live 400s
2026-09-21redeem ignores amount (0.5 redeemed 100 %); deleted posts still refund; leaderboard walk stops near page 120.verified
2026-09-20Endpoint catalog marks (feed, market, trades, invest, redeem, users/*, leaderboard, chat, trade-with, cards/trades, presign, PostgREST DM tables); access-token lifetime ≈ 24 h; tape carries potAfter/levelAfter, ~10-aura floor.verified
2026-09-17Card-trade item format {"itemType":"card","id":"<userCardId>"}; validation order; upload presign flow.verified
Import-post floor of 100; rate-limit window; Retry-After; refresh-token reuse behaviour; sell-lock semantics; status/change24h/counter meanings; card catalog/boxes/market/rewards shapes; create_user:true provisioning; new-DM creation; tape transport.unverified
Field names marked inferred come from readers that accept several spellings (listedAt/listed_at, creator_verified, followers_count, …). Treat them as optional.inferred

The platform changes without notice. A field that is present today may be renamed tomorrow; code against this page should tolerate missing fields and both casings.

9. index

Every endpoint, parameter, response field, error code, concept and term on this page, alphabetically. Type / to search the same list from the sidebar.