Docs
SIN is private, uncensored inference on open weight models. A wallet is the account, credits are USDG on Robinhood Chain, and nothing you say is logged.
How It Works
- Sign in is one wallet signature. No email, no phone, no name.
- Credits are bought with USDG in one transaction from the wallet. 1 USDG is 1 credit.
- Every request in the app or through the API is paid from credits, by the token.
- New visitors get a few free replies a day on one model, no wallet needed.
- $SIN stakers earn USDG from every credit purchase.
| Part | What it is |
|---|---|
| App | The chat at https://sin.run/app. Wallet sign in, model picker, streaming answers, agent mode with chain tools, history kept in the browser. |
| API | An OpenAI compatible endpoint at https://api.sin.run. Same models, same credits, your own code. |
| CreditPurchase | The contract that takes USDG and splits it in public. Credits are posted from its events. |
| SinStaking | The contract that pays $SIN stakers a share of every purchase, in USDG. |
| Robinhood Chain | Where the money moves. Gas is paid in ETH, well under a cent per transaction. |
Privacy
What follows is what the code does. Enforced by code, not by policy.
- No account. Sign in is a wallet signature. The service knows an address and nothing about who holds it.
- No transcripts. Prompts and answers pass through in memory and are gone when the stream ends.
- No keys. The service never holds a private key and never sends a transaction.
- Conversations live in the browser. Synced history is ciphertext under a key derived from your wallet signature. The server sees sizes and dates.
Where Your Words Go
A prompt leaves your browser over TLS and reaches the API through Cloudflare. The origin only answers requests that carry a secret header the Cloudflare zone adds, so the edge cannot be stepped around. The API rebuilds each request from a fixed list of fields before anything is forwarded: messages, sampling settings, output limits, stop sequences, response format, tools and reasoning settings pass. Everything else is dropped, including the user and metadata fields OpenAI clients like to fill in. The rebuilt request goes to the model provider under one API key shared by every SIN user, with a random request id. The answer streams back the same way.
While this happens the API holds the request and the answer in memory. It writes neither to a log, a database, an error tracker or a cache. When the stream ends they are gone. The one exception you control is history sync, below, which stores what your browser already encrypted.
Cloudflare and the host carry the traffic and can see it in transit, as they can for most of the web. SIN hands neither of them your identity, and nothing SIN runs writes down what passes through them. The model provider is held to zero data retention.
What The Service Keeps
| Kept | Why | For how long |
|---|---|---|
| Wallet address | It is the account | While the account exists |
| Credit balance and ledger | Every credit in and out, double entry, append only | Permanent |
| Purchases | Transaction hash, amount and status of each purchase | Permanent |
| Request metadata | Model, token counts, cost, latency, status. Never the text | 30 days, then folded into daily totals. Deleted at once with history off |
| Encrypted history blobs | Sync between devices | Until you delete them |
Logs, Errors And Metrics
Each request produces one log line: a request id, the method, the route pattern, the status, the duration and the Cloudflare ray id. Inference lines add the model, token counts, the charge and upstream latency. A user id appears only on warnings and errors. A wallet address is never logged, and neither is an IP. Lines are kept for seven days.
The error tracker receives the exception, its stack, the process name and the request id. Request data, headers and user fields are stripped before an event leaves the process. Metrics are counts and timings by route and model, with no per user labels.
History
The app keeps conversations in the browser's own storage, per wallet, so two wallets on one machine never see each other's chats. Anyone who can open your browser profile can read them there, as with any local data.
Sync between devices is off until you turn it on for a wallet. When you do, the app asks your wallet to sign the fixed text SIN history key v1 and derives a 256 bit key from that signature with HKDF SHA 256, salted with sin-history-v1 and bound to your address. Each conversation is encrypted with AES 256 GCM under a fresh random 12 byte nonce, with the conversation id as authenticated data and a version byte in front. An index blob lists titles and ids so the sidebar can render without downloading everything. The server sees ciphertext, sizes, versions and timestamps.
The derived key is kept in the browser as a non extractable key, so you sign once per device, not on every reload. Turning sync off forgets it. There is no copy of the key on the server, so losing the wallet means losing the synced history. Smart contract wallets cannot sign deterministically, so for them the key is generated on the device and history stays per device. One call deletes every blob.
Agent Mode
A request runs no tool unless it names them. In the app that is the agent switch in the composer. With tools on, the model can ask the API to search the web, read a page, do arithmetic, read the clock, read balances and transactions on Robinhood Chain, look up a token, a Pons coin or a live price, or read your own credit balance. Each of those runs on SIN servers.
A web search goes to Brave from a SIN address under a SIN key. Brave sees the query and nothing about you. A page fetch leaves from a SIN address with a plain bot user agent, so the site sees SIN, not you, and the fetcher refuses private and internal networks. Tool calls and their arguments appear in your own stream so the app can show what the model is doing. They are not stored. The database keeps how many calls a request made. Nothing in agent mode signs, sends, stores or executes anything for you, and a fetched page is text the model reads, never an instruction the API follows.
The One Line
SIN does not moderate opinions, language or topics. It holds one line: sexual content involving minors, sexual images of real people without their consent, and working tools built to steal from people, such as wallet emptying contracts, phishing kits and malware. A request that crosses it is answered with refused before any credits are reserved, and costs nothing. The check reads the latest message in memory and keeps nothing.
Sign In
Sign In With Ethereum. The app fetches a nonce bound to the address, the wallet signs a message naming the app, the chain and the nonce, and the API opens a 30 day session. No refresh: a new signature after that.
sin.run wants you to sign in with your Ethereum account:
0xAbC0000000000000000000000000000000000001
Sign in to SIN
URI: https://sin.run/app
Version: 1
Chain ID: 4663
Nonce: 3f9c1a7d2b0e4c6a8d1f2e3b
Issued At: 2026-09-19T20:00:00.000Z
EOAs and smart contract wallets both work. Credits bought before the first sign in are posted on that sign in.
API Keys
Keys start with sin_, are shown once, never expire and survive sign out. A session token and a key are interchangeable as the bearer. 20 sessions per wallet, oldest evicted. 10 active keys.
Credits
USDG On Robinhood Chain
Credits are bought with USDG on Robinhood Chain. USDG gets there from Robinhood Wallet or over a bridge such as Across, Relay, Stargate, LiFi or the Arbitrum bridge. Gas is ETH, well under a cent per transaction.
Buying
A purchase is one transaction to the CreditPurchase contract. The app signs a permit, so approval and purchase land together. Approve then purchase works too. Minimum 5 USDG, no maximum.
Credits are posted after 64 confirmations, a few seconds. Above 500 USDG the watcher waits for a finalized block, which can take minutes. The purchase list shows each one move from seen to credited.
| Status | Meaning |
|---|---|
| seen | Observed on chain, waiting for confirmations |
| confirmed | Confirmed, credit in progress |
| credited | The credits are on the balance |
| below_minimum | Under 5 USDG, not credited |
| unmatched | Not credited automatically, for example USDG sent straight to the treasury. Handled by hand |
| orphaned | The block was reorganised away before confirmation. Nothing happened |
Bonuses
Volume tiers come from the API and the highest tier at or below the purchase applies.
| Purchase | Bonus |
|---|---|
| 50 USDG or more | +5% |
| 100 USDG or more | +10% |
| 250 USDG or more | +15% |
| 500 USDG or more | +20% |
A promo code credits a fixed amount at once, or parks a percentage bonus for the next purchase.
One Way
Credits belong to the wallet that bought them. No transfer, no withdrawal, no refund.
Free Replies
Visitors without a wallet get a free session: a small number of replies a day on one model, with a capped output length. The session is a token in the browser, not an account. Free replies stop for everyone once the day's free budget is spent, and start again the next day.
Staker Allowance
Wallets with $SIN staked can claim a weekly credit allowance sized to their stake. It is off until the allowance rate is published; /purchase/info and the staking page show it when it is on.
Billing
Prices are per million tokens, input and output separately, per model. The model list carries them.
Before a request runs the API reserves the estimated cost times 1.2. The estimate counts input characters plus an output budget. With max_tokens that is the budget; without it, the model ceiling shrunk to what the balance can pay, so an answer is only cut where the money runs out. On finish the API charges actual usage, never more than reserved, and releases the rest.
The balance shows spendable and reserved. Failures before the first token cost nothing. Stopping a stream charges what was delivered. An idle reservation is released after ten minutes. insufficient_balance means even 64 output tokens are not affordable, or the requested output budget exceeds the balance.
The ledger holds every credit movement. The request history holds model, tokens, cost and latency, never text. With history off those rows are deleted as each request settles.
Models
Open weight models, served as released, with the uncensored variants where they exist. The live list with context sizes and prices is at /v1/models.
The model list is published at launch.
Encrypted History
Conversations live in the browser. Sync between devices goes through the API as ciphertext.
The wallet signs a fixed message once per device and HKDF turns the signature into a 256 bit key the browser keeps as a non extractable key. Each conversation is a blob under AES GCM with a fresh nonce. An index blob lists the conversations so the sidebar renders without fetching everything. The server stores blobs by id and version and sees size, timestamp and count. There is no recovery key.
| Quota | Limit |
|---|---|
| One blob | 512 KiB |
| All blobs per wallet | 64 MiB |
| Blobs per wallet | 2000 |
One call wipes everything. The history switch in settings controls request metadata only and does not touch the blobs.
API
OpenAI chat completions dialect. Any OpenAI client pointed at the base URL with an API key works.
Base URL https://api.sin.run/v1
Conventions
- Every response is JSON, except a streaming completion, which is an event stream.
- Every error is an object with a stable code, a message and a request id:
{"error":{"code":"...","message":"...","request_id":"..."}}. Branch on the code. - Money and token amounts are decimal strings with six decimals, never floats.
- Every response carries an
X-Request-Idheader. - Auth is a bearer token: a session, an API key, or a free session token.
Endpoints
Auth
GET /auth/nonce · No auth Issues a nonce for the sign in message. Bound to the wallet, lives five minutes, used once. Query: wallet (address, required).
{ "nonce": "3f9c1a7d2b0e4c6a8d1f2e3b", "expires_at": "2026-09-19T20:05:00.123456Z", "chain_id": 4663 }
Errors: bad_wallet 400
POST /auth/verify · No auth Takes the signed message and opens a session for 30 days. A wallet that bought credits before its first sign in is credited inside this call. Body: message (the full message exactly as signed, newlines included), signature (65 byte hex).
{ "token": "e3b0c442…", "user": { "id": "8c2b1a4e-…", "wallet": "0xabc0…0001" } }
Errors: bad_message 400/401, bad_nonce 401, bad_signature 401, banned 403
POST /auth/logout · Session only Ends the session. API keys are untouched. 204.
Free Sessions
POST /free/session · No auth Opens a free session for the free model. Returns 404 when free replies are off.
{ "token": "free_…", "replies_left": 3, "model": "…", "expires_at": "2026-09-20T20:00:00Z" }
Errors: rate_limited 429, free_exhausted 429
Account
GET /me · Session or API key The account with available, reserved and spendable balance.
{
"id": "8c2b1a4e-…",
"wallet": "0xabc0…0001",
"balance": { "available": "42.500000", "reserved": "0.120000", "spendable": "42.380000" },
"tier": { "min_amount": "50", "bonus_pct": "5" },
"no_history": false,
"created_at": "2026-09-19T12:00:00.000000Z"
}
Errors: unauthorized 401, banned 403
GET /me/ledger · Session or API key Every credit movement, newest first. Positive amounts are credits, negative are charges. Keyset pagination on the entry id. Query: cursor (entries with an id below this), limit (1 to 200, default 50).
{
"entries": [
{ "id": 1042, "txn_id": "2a7c…", "amount": "-0.012000", "type": "inference_charge",
"ref_type": "inference", "ref_id": "5f0b…",
"metadata": { "model": "…", "input_tokens": 12, "output_tokens": 240 },
"created_at": "2026-09-19T20:01:12.410000Z" }
],
"next_cursor": 1041
}
GET /me/deposits · Session or API key On chain purchases of the wallet, newest first, at most 200, including purchases made before the first sign in.
{ "deposits": [ { "id": "c1d2…", "tx_hash": "0x9d4f…", "block_number": 63180200, "source": "purchase",
"amount": "10.000000", "status": "credited", "created_at": "…", "updated_at": "…" } ] }
GET /me/requests · Session or API key Inference metadata only, never text. Rows older than 30 days fold into daily totals. Query: cursor (timestamp from the previous next_cursor), limit (1 to 200, default 50).
{ "requests": [ { "id": "5f0b…", "model": "…", "status": "completed", "reserved": "0.015000",
"input_tokens": 12, "output_tokens": 240, "charged": "0.012000", "latency_ms": 1840, "ttft_ms": 320,
"tool_calls": 0, "error_code": null, "created_at": "…", "finished_at": "…" } ], "next_cursor": "" }
POST /me/settings · Session or API key Changes the fields present in the body. no_history: true deletes finished request rows at once. 204. Errors: bad_body 400
POST /me/allowance/claim · Session or API key Claims the weekly staker allowance for the signed in wallet.
{ "credited": "3.000000", "next_claim_at": "2026-09-26T20:00:00Z" }
Errors: allowance_off 404, allowance_not_yet 409
GET /me/api-keys · Session or API key Lists active keys, newest first, without the plaintext.
POST /me/api-keys · Session or API key Creates a key. The plaintext is returned once. Ten active keys per wallet. Body: name (a label).
{ "id": "7b1e…", "key": "sin_4c1f9e2a…", "name": "agent" }
Errors: bad_body 400, too_many_keys 409
DELETE /me/api-keys/{id} · Session or API key Revokes a key. Idempotent. 204.
Encrypted History
GET /me/history · Session only Lists the encrypted blobs, newest first, with sizes, versions and the quota.
{ "items": [ { "id": "0d9c…", "kind": "conversation", "size": 18432, "version": 3, "updated_at": "…" } ],
"quota": { "used_bytes": 18432, "max_bytes": 67108864, "max_blobs": 2000, "max_blob_bytes": 524288 } }
GET /me/history/{id} · Session only Returns the raw ciphertext of one blob. Headers X-History-Version and Last-Modified.
PUT /me/history/{id} · Session only Creates or replaces a blob with raw ciphertext in the body. Query kind (conversation or index). Header If-Match with the expected current version makes the write conditional.
{ "id": "0d9c…", "version": 4 }
Errors: version_conflict 409, blob_too_large 413, quota_blobs 507, quota_bytes 507
DELETE /me/history/{id} · Session only. Deletes one blob.
DELETE /me/history · Session only. Deletes every blob of the wallet.
Purchase
GET /purchase/info · No auth Everything a purchase needs. Values change only on redeploy.
{
"chain_id": 4663,
"purchase_contract": "Published at launch",
"token_address": "0x5fc5360d0400a0fd4f2af552add042d716f1d168",
"token_decimals": 6,
"staking_contract": "Published at launch",
"staking_enabled": true,
"sin_address": "Published at launch",
"sin_decimals": 18,
"min_amount": "5.00",
"confirmations": 64,
"discount_tiers": [
{ "min_amount": "50", "bonus_pct": "5" }, { "min_amount": "100", "bonus_pct": "10" },
{ "min_amount": "250", "bonus_pct": "15" }, { "min_amount": "500", "bonus_pct": "20" }
],
"split": { "staking_bps": 1000, "buyback_bps": 1000 }
}
Staking
GET /staking/stats · No auth Public staking totals, cached for 30 seconds.
{
"tvl_sin": "1250000",
"total_distributed_usdg": "5120.5",
"distributed_7d_usdg": "310.2",
"distributed_30d_usdg": "1200",
"total_paid_usdg": "4900.1",
"stakers": 87,
"total_burned_sin": "40000",
"purchased_total_usdg": "51205",
"purchased_24h_usdg": "1300",
"sin_price_usdg": "0.0012",
"updated_at": "2026-09-19T20:00:00Z"
}
GET /staking/events · No auth The staking activity feed from the chain watcher, newest first. Amounts are in SIN for staked, unstaked and burned, in USDG for claimed and reward_deposited. Query: address (only this wallet), cursor, limit (1 to 200, default 50).
{ "events": [ { "id": 12, "kind": "reward_deposited", "address": "0x…", "amount": "0.5",
"tx_hash": "0x…", "block_number": 63180200, "at": "2026-09-19T10:00:00Z" } ], "next_cursor": "" }
Errors: bad_wallet 400, rate_limited 429
Promo
POST /promo/redeem · Session or API key Redeems a code. A fixed credit lands at once; a percentage bonus waits for the next credited purchase. One pending bonus at a time. Body: code (case does not matter).
{ "kind": "deposit_bonus_pct", "credited": "0", "bonus_pct": "20" }
Errors: promo_invalid 404, promo_exhausted 409, promo_used 409, promo_pending 409
Inference
GET /v1/models · No auth Enabled models in the OpenAI list shape, with context window and prices per million tokens in credits.
{ "object": "list", "data": [ { "id": "…", "object": "model", "owned_by": "sin", "max_context": 262144,
"tee": false, "tools": true, "free": false,
"pricing": { "input_per_1m": "…", "output_per_1m": "…" } } ] }
POST /v1/chat/completions · Session, API key or free session OpenAI chat completion, streaming or not. The upstream request is rebuilt from an allowlist of fields, one choice per request. Credits are reserved before the call and settled after it.
| Field | Type | |
|---|---|---|
| model | string, required | A model id from the model list |
| messages | array, required | OpenAI chat messages with the roles system, user, assistant and tool |
| stream | boolean | Event stream instead of one JSON response |
| max_tokens | integer | Output budget. Also caps the credit reservation |
| temperature, top_p, top_k, min_p | number | Passed through unchanged |
| repetition_penalty, presence_penalty, frequency_penalty | number | Passed through unchanged |
| stop | string or array | Passed through unchanged |
| seed | integer | Passed through unchanged |
| response_format | object | Passed through unchanged |
| tools | array | Your own function tools, passed through unchanged |
| reasoning_effort | string | Passed through unchanged |
| sin_tools | array of strings | SIN server tools for agent mode, see below |
{ "model": "…", "messages": [ { "role": "system", "content": "You are terse." }, { "role": "user", "content": "Hello" } ],
"stream": true, "max_tokens": 512 }
With stream the response is OpenAI chunks, a final usage chunk, then data: [DONE]. A stream without that marker was cut off. Every completion carries an X-Inference-Id header that matches the request history and the ledger.
Errors: invalid_request 400, context_too_long 400, refused 400, unauthorized 401, insufficient_balance 402, model_not_found 404, body_too_large 413, rate_limited 429, free_exhausted 429, upstream_unavailable 503, overloaded 503
Agent Mode Over The API
Name the server tools in sin_tools. The API runs them between model turns (at most six rounds) and adds two kinds of chunk to the stream so you can show the work. Plain OpenAI clients that do not set sin_tools never see them.
data: {"object":"sin.tool_call","id":"t1","name":"pons_token","arguments":{"query":"SIN"}}
data: {"object":"sin.tool_result","id":"t1","name":"pons_token","ok":true,"summary":"…","sources":["…"]}
| Tool | What it does |
|---|---|
| web_search | Brave web search |
| fetch_url | Reads one public page as text |
| calc | Arithmetic |
| now | Date and time in a timezone |
| wallet_balance | ETH and token balances of an address on Robinhood Chain |
| transaction | A transaction and its receipt |
| token_info | Name, symbol, decimals, supply and a holder balance of a token |
| pons_token | A coin on the Pons launchpad by symbol or address |
| dex_price | Live price, FDV and liquidity from DexScreener |
| staking_stats | The SIN staking totals |
| account_balance | Your own credit balance |
If the rounds run out the reply finishes with finish_reason: "tool_rounds" and answers from what it found.
Examples
curl https://api.sin.run/v1/chat/completions \
-H "Authorization: Bearer $SIN_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"MODEL_ID","messages":[{"role":"user","content":"hi"}],"stream":true}'
from openai import OpenAI
client = OpenAI(base_url="https://api.sin.run/v1", api_key=SIN_KEY)
stream = client.chat.completions.create(model="MODEL_ID", messages=[{"role": "user", "content": "hi"}], stream=True)
for chunk in stream:
if chunk.choices:
print(chunk.choices[0].delta.content or "", end="")
import OpenAI from 'openai';
const client = new OpenAI({ baseURL: 'https://api.sin.run/v1', apiKey: process.env.SIN_KEY });
const stream = await client.chat.completions.create({ model: 'MODEL_ID', messages: [{ role: 'user', content: 'hi' }], stream: true });
for await (const chunk of stream) process.stdout.write(chunk.choices[0]?.delta.content ?? '');
Limits
| What | Limit |
|---|---|
| Chat completions | 60 per minute, burst 20, per wallet across all sessions and keys |
| Sign in endpoints | 10 per minute per wallet |
| Promo codes | 5 per minute |
| Request body | 4 MiB |
| Sessions | 20 per wallet, oldest evicted |
| API keys | 10 active per wallet |
| Minimum purchase | 5 USDG |
Error Codes
| Code | Status | Meaning |
|---|---|---|
| unauthorized | 401 | Missing, expired or revoked token |
| banned | 403 | The account is disabled |
| bad_wallet | 400 | The wallet is not a hex address |
| bad_nonce | 401 | The nonce expired or was already used |
| bad_signature | 401 | The signature does not match the wallet |
| bad_message | 400, 401 | The sign in message is malformed or names the wrong domain or chain |
| bad_body, invalid_json | 400 | The body is not valid JSON or has the wrong shape |
| bad_id | 400 | The id is not a UUID |
| too_many_keys | 409 | Ten active keys already exist |
| not_found | 404 | No such blob |
| version_conflict | 409 | The blob moved on the server since the version in the request |
| blob_too_large | 413 | A blob over 512 KiB |
| quota_blobs, quota_bytes | 507 | The blob count or the byte quota of the wallet is full |
| promo_invalid | 404 | Unknown, not started or expired code |
| promo_exhausted | 409 | The code has reached its global limit |
| promo_used | 409 | The wallet already used this code |
| promo_pending | 409 | A purchase bonus is already waiting |
| allowance_off | 404 | The staker allowance is not on |
| allowance_not_yet | 409 | This week's allowance was already claimed |
| free_exhausted | 429 | No free replies left for this session or for today |
| refused | 400 | The request crosses the one line. Not charged |
| invalid_request | 400 | Missing model or messages, or more than one choice |
| context_too_long | 400 | The conversation exceeds the model context |
| model_not_found | 404 | Unknown or disabled model |
| insufficient_balance | 402 | Not enough credits for this request |
| body_too_large | 413 | Over 4 MiB |
| rate_limited | 429 | Too many requests |
| client_closed | 499 | The client went away before the model answered. Not charged |
| upstream_unavailable | 503 | The model host is failing. Not charged |
| overloaded | 503 | At capacity. Not charged |
| upstream_no_credits | 503 | The upstream account is empty. Not charged |
| upstream_unreachable | 502 | The model host did not answer. Not charged |
| upstream_unconfigured | 503 | No attestation upstream is configured |
| attestation_unsupported | 502 | The enclave report came in a shape the API does not project |
| upstream_rejected | 400 | The model host rejected the request; its message is passed through |
| internal | 500 | Server error. The request id finds it in the logs |
Contracts
Two contracts handle all the money on Robinhood Chain. CreditPurchase takes USDG and turns it into credits. SinStaking pays $SIN stakers a share of every purchase. Both are open source, verified on the explorer, and cannot be upgraded or paused.
Where Each Purchase Goes
Every purchase is split the moment it lands. Nothing sits inside the contract between transactions. The treasury share pays for inference.
| Share | Goes to |
|---|---|
| 10% | SinStaking, paid out to $SIN stakers in USDG |
| 10% | The buyback wallet, which buys $SIN and burns it |
| 80% | The treasury wallet, which pays for inference |
A worked example. Someone buys 100 USDG of credits:
| USDG | |
|---|---|
| To SinStaking, shared by everyone staked at that moment | 10 |
| To the buyback wallet, spent on $SIN that is then burned | 10 |
| To the treasury, which pays the model provider | 80 |
| Credits the buyer receives (100 plus the 10% tier bonus) | 110 |
The landing page numbers come straight from the staking stats: annualised buybacks are the last 24 hours of purchases times the buyback share times 365, and the staking APR is the last 24 hours of purchases times the staking share times 365, divided by the dollar value of all staked $SIN. So if 1,000 USDG of credits sell in a day and 50,000 dollars of $SIN is staked, stakers receive 100 USDG that day, about 73% a year, and 100 USDG of $SIN is bought and burned.
With nothing staked the staking share goes to the treasury, as does rounding dust. The owner can change the split; every change emits an event and applies from the next purchase. If any leg fails the whole purchase reverts. The contract never holds a balance. Tokens sent to it by mistake can be returned by the owner.
Staking $SIN
Staked $SIN earns USDG. Each distribution is divided among everyone staked at that moment, pro rata. USDG enters with every credit purchase, and anyone can call distribute with their own USDG.
Rewards accrue as they come in and stay claimable after a withdrawal. Stakers who join after a distribution do not share in it.
$SIN is locked for 24 hours after the most recent stake, and adding to a stake relocks the whole balance. After the lock, withdraw any amount, or exit to withdraw everything and claim in one transaction.
The lock is the flash loan defence. A stake cannot leave in the transaction that created it, and a full day makes just in time staking with borrowed money uneconomic.
SinStaking has no owner, no pause, no upgrade and no admin function. The lock and the tokens are fixed at deployment.
Addresses
| Network | Robinhood Chain |
| Explorer | robinhoodchain.blockscout.com |
| USDG | 0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168 |
| $SIN | Published at launch |
| SinStaking | Published at launch |
| CreditPurchase | Published at launch |
Source is verified on the explorer.
For Developers
The interfaces below are stable. Accounts are credited from the Purchased event and nothing else.
CreditPurchase
| Function | What it does |
|---|---|
| purchase(amount) | Pulls the amount of USDG from the caller and splits it. Needs a prior approval |
| purchaseWithPermit(amount, deadline, v, r, s) | Same with a permit signed by the caller, no approval step. A used permit with an existing allowance still goes through |
| MIN_PURCHASE() | 5000000, which is 5 USDG in six decimal units |
| STAKING_BPS(), BUYBACK_BPS() | The current split in basis points. Live values, not constants |
| usdg(), staking(), treasury(), buyback() | The token and the three destinations |
Purchased(buyer, amount, toStaking, toBuyback, toTreasury) is emitted once per purchase with the amounts actually moved, and the three shares sum to the amount. The buyer is the sender; there is no purchase on behalf of another address. SplitChanged(stakingBps, buybackBps) and AddressChanged(key, oldAddress, newAddress) announce every owner change.
Permit Domain
USDG does not implement ERC 5267, so the permit domain is fixed by hand. The spender is CreditPurchase and the nonce comes from nonces(owner) on USDG.
| name | Global Dollar |
| version | 1 |
| chainId | 4663 |
| verifyingContract | 0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168 |
SinStaking
| Function | What it does |
|---|---|
| stake(amount) | Stakes $SIN and resets the lock to now plus 24 hours |
| withdraw(amount) | Returns staked $SIN. Reverts with StakeLocked before the unlock time |
| claim() | Sends the accrued USDG to the wallet |
| exit() | Withdraws the full stake and claims in one call |
| distribute(amount) | Anyone. Pulls USDG from the caller and shares it among current stakers. Reverts with NoStakers when nothing is staked |
| skim() | Anyone. Allocates USDG that was sent to the contract directly instead of through distribute |
| earned(address) | USDG the address can claim right now |
| unlockTime(address) | Earliest timestamp the address can withdraw |
| totalStaked() | All $SIN in the contract |
Events: Staked, Withdrawn, RewardPaid and RewardAdded(from, amount). The sum of RewardAdded from CreditPurchase equals the sum of toStaking across all purchases.
What Cannot Change
- Neither contract can be upgraded or paused.
- SinStaking has no owner and no admin functions.
- The CreditPurchase owner can change the split and the three destinations; every change emits an event. The owner cannot pause the contract or touch any wallet or stake.
- Ownership moves in two steps; the new owner has to accept.
- Reentrancy guards on every state changing function, OpenZeppelin SafeERC20 on every transfer. Reward accounting is the Synthetix StakingRewards pattern, with the accumulator advancing per distribution instead of per second.
For Agents
The API is OpenAI compatible, so any agent framework that takes a base URL and a key works. Buy credits once from a wallet, create a key in the app under Settings, API keys, and point the client at SIN.
export OPENAI_BASE_URL="https://api.sin.run/v1"
export OPENAI_API_KEY="sin_…"
A SIN agent skill that gives an agent its own wallet, buys credits and signs in without a human is coming soon.
FAQ
Is anything it says financial advice?
No. SIN is software and it can be wrong. It tells you what the chain shows and what it thinks; the decision is yours.
Is it really uncensored?
It runs open weight models without a refusal layer and it does not lecture. It holds one line, described under Privacy: sexual content involving minors, sexual images of real people without consent, and working tools built to steal from people.
What do you keep about me?
Your wallet address, your credit ledger and your purchases. Never the text of what you ask or what it answers. See What The Service Keeps.
How do $SIN stakers earn?
Every credit purchase sends 10% of it to SinStaking, shared by everyone staked at that moment and paid in USDG. Another 10% buys $SIN and burns it.
Do I need a wallet to try it?
No. The ask box on the home page gives a few free replies a day. A wallet signs you in for every model and your own credits.
Where is the contract address?
On the SIN X account, posted first at launch, and on this site. Anything that does not match both is not us.