# Sexy Models - account signup and sign-in

API base: https://api.sexymodels.fun

Email and Solana wallet signup create ordinary user accounts with the same
permissions. These APIs work for humans, scripts, and agents. There is no actor
type to select. Signup completion returns an account session, not an API key.
Use that session to create a named inference key separately.

## Wallet signup or sign-in

You need a Solana wallet whose messages you can sign. A new wallet also needs
exactly 1 USDC on Solana mainnet and SOL for network fees. Native USDC mint:
`EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`. Keep the private key locally.

1. POST `https://api.sexymodels.fun/v1/signups/wallet` with `{"wallet":"YOUR_PUBLIC_KEY"}`.
2. Save `signup_id`, `message`, `expires_s`, `payment_required`, and `invoice`.
3. Sign the exact UTF-8 bytes of `message` using this wallet's Ed25519 key.
   Base64-encode the 64-byte signature. Do not trim or change the message.
4. If `payment_required` is true, transfer exactly `invoice.amount_usdc` of
   native USDC before `invoice.expires_s`. Use `invoice.payment_url` with a
   Solana Pay wallet, or build a native USDC transfer to `invoice.recipient`
   with `invoice.reference` appended to the transfer instruction as a read-only,
   non-signing account. The transfer must be the last instruction. A memo or
   address-only transfer is insufficient. The full $1 becomes inference credit.
   SOL fees are separate. No free email signup
   credit is granted to wallet accounts. Holding USDC is not payment: funds must
   actually move to our receiving wallet. Signing a message does not move funds.
5. POST `https://api.sexymodels.fun/v1/signups/wallet/complete` with:

```json
{"signup_id":"...","signature":"BASE64_MESSAGE_SIGNATURE","transaction_id":"SOLANA_TRANSACTION_SIGNATURE","session_delivery":"bearer"}
```

For new wallets, `transaction_id` is optional: the server can discover the
payment by reference. Include the signature to speed up lookup. You must still
sign the account challenge to complete signup.

Returning wallets receive `payment_required: false` and `invoice: null`.
Omit `transaction_id`; prove ownership using the fresh challenge. They do not
pay again to sign in, even if their balance is zero.

HTTP 202 with `status: PENDING` means payment has not finalized. Retry the same
completion request after `retry_after_s` (normally 10 seconds). Never send a
second payment because a request timed out. HTTP 201 creates a new account;
HTTP 200 signs in an existing account. Both return `user_account`, `created`,
and `session`. Save `session.token`; it expires after one hour by default.

If an on-time payment's challenge expires before completion, start a new
challenge with `{"wallet":"YOUR_PUBLIC_KEY","recover_signup_id":"ORIGINAL_SIGNUP_ID"}`.
Sign the new message and submit the original transaction signature. The original
invoice/payment window is preserved. Do not transfer again. If a successful
completion response is lost, sign a fresh challenge to get another session.

For a local Solana CLI-format keypair, sign with Python:

```python
import base64, json
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
signup = json.load(open("signup.json"))
keypair = bytes(json.load(open("wallet.json")))
key = Ed25519PrivateKey.from_private_bytes(keypair[:32])
signature = base64.b64encode(key.sign(signup["message"].encode("utf-8"))).decode()
```

## Email signup or sign-in

1. POST `https://api.sexymodels.fun/v1/signups/email` with `{"email":"you@example.com"}`.
2. Read the fresh verification code from your email inbox. Codes expire in
   10 minutes; five incorrect attempts invalidate a signup attempt.
3. POST `https://api.sexymodels.fun/v1/signups/email/complete` with
   `{"signup_id":"...","code":"...","session_delivery":"bearer"}`.
4. Save `session.token`. A new account is created only after successful proof.
   Returning users must verify a fresh code and receive their existing account.
   Signup credit is granted only once per eligible inbox, never on sign-in.

Email requests are limited to five per source address per hour and three per
email per hour. Wallet challenges are limited to five per source address per
hour. Challenges are single-use and expire; rate limits return HTTP 429.

## Account sessions and API keys

Pass `Authorization: Bearer sm_session_...` for account and key management:

| Method | Route | Purpose |
|--------|-------|---------|
| GET | `/v1/session` | Read the signed-in account and session expiry. |
| DELETE | `/v1/session` | Revoke this session and sign out. |
| POST | `/v1/api_keys` | Create a key with `{"name":"My application"}`. |
| GET | `/v1/api_keys` | List IDs, names, prefixes, timestamps and revocation status. |
| DELETE | `/v1/api_keys/KEY_ID` | Revoke one of your keys immediately. |

POST `/v1/api_keys` returns a permanent `sm_...` key, shown once. The server
stores its hash, not the raw secret. GET never returns key secrets. Existing
`sis_...` keys remain valid in this deployment. Inference keys cannot create,
list, or revoke keys or authenticate `/v1/session`.

Expired sessions require a fresh email code or wallet signature. Logout revokes
only the current session, not your inference keys. Key management and funding
remain available at zero balance. To rotate a key, create a replacement, update
your client, then revoke the old key.

Browser clients use `session_delivery: "cookie"`. Send requests from the site
origin; completion sets a Secure, HttpOnly, SameSite=Lax cookie and returns no
session token in JSON. GET `/v1/session` returns `session.csrf_token`; send it
as `X-CSRF-Token` on authenticated changes. Cookies must use HTTPS. Cookie and
Bearer sessions expire and are revoked on the server.

## Inference and more credit

Pass `Authorization: Bearer sm_...` for inference:

- GET `https://api.sexymodels.fun/v1/models` lists supported models without authentication.
- POST `https://api.sexymodels.fun/v1/chat/completions` or `/v1/responses` generates text.
- GET `https://api.sexymodels.fun/v1/user_accounts/me` reads your credit balance.
- POST `https://api.sexymodels.fun/v1/crypto/topups` with `{"chain":"solana","amount_cents":2000}`
  creates an exact 20.00 USDC payment request ($5-$1,000). Send an
  `Idempotency-Key` header; reuse it with the same parameters after a timeout.
- Save the returned `id`, `reference`, `payment_url`, and `expires_s`.
  Humans can use the dashboard's browser-wallet button, QR code, or payment link.
- For CLI payments, POST `https://api.sexymodels.fun/v1/crypto/topups/INVOICE_ID/transaction`
  with `{"wallet":"YOUR_PUBLIC_KEY"}`. It returns a base64 unsigned Solana
  transaction and `last_valid_block_height`. Decode and inspect it, sign locally,
  then broadcast on Solana mainnet. Keep the signed bytes and signature before
  submitting, so a retry can only rebroadcast the same transfer.
- GET `https://api.sexymodels.fun/v1/crypto/topups/INVOICE_ID` checks settlement. The server scans
  payment references every ten seconds; no manual signature submission is needed.
- Optional POST `https://api.sexymodels.fun/v1/crypto/topups/INVOICE_ID/verify` with
  `{"transaction_id":"SOLANA_TRANSACTION_SIGNATURE"}` starts direct lookup.

Payment states are `pending`, `confirming`, `credited`, or `expired`.
Balance and top-up operations also accept account sessions and work at zero
balance. Finalized transfers are credited once. Verification checks network,
native token, recipient, exact amount, transfer reference, successful execution,
and payment time. Late, partial, excess, or unreferenced payments require manual
reconciliation; never automatically send a second transfer after a timeout.
An on-time transfer can finalize after expiry. Automatic lookup continues for
24 hours after expiry; optional signature lookup remains available afterward.

The repository's CLI helper uses a local Solana CLI JSON keypair and token file:

```sh
python -m src.ops.pay_usdc --token-file ./sm-token.txt --wallet ./wallet.json   --amount 20 --state ./payment-20.json
```

Install the repository's `requirements.lock` first. Reuse the same state file to
resume a purchase after a network error. Use a new state file for a new purchase.
The helper checks the unsigned transaction against the requested USDC transfer,
keeps private keys local, and never re-signs an uncertain payment.

API reference: `https://api.sexymodels.fun/docs`. OpenAPI: `https://api.sexymodels.fun/openapi.json`.
