Official Python SDK for AltsCodex DeOAuth — a decentralized identity layer
that bridges OAuth with on-chain account abstraction. This package is the
async-Python port of @altscodex/sdk
(npm).
📚 Docs · Support · Sign up — developers.altscodex.com 🏠 Platform — altscodex.com
- Async-native — built on
httpx.AsyncClientandasyncio.Future - FastAPI-friendly —
handle_callback(request)accepts a StarletteRequest - Concurrency-safe — state-keyed pending map, per-request timeout, graceful shutdown
- Secret-safe —
client_secretis held in a name-mangled private attribute - Test-friendly — accepts an injected
httpx.AsyncClient(useMockTransport) - Zero hidden state — the SDK reads no environment variables; you pass options explicitly
- What this SDK does
- When to use it (and when not to)
- Installation
- Quick start
- How the full flow works
- API reference
- Integration patterns
- Concurrency model
- Configuration
- Error handling & HTTP status mapping
- Testing your integration
- Local development
- Security
- Common pitfalls
- Comparison with the JavaScript SDK
- Publishing this SDK
- Contributing
- Resources
- License
AltsCodex DeOAuth is a three-party OAuth flow extended with an on-chain identity layer. The browser obtains a short-lived JWT from the AltsCodex platform server, then your backend uses this SDK to exchange that JWT for the user's slot information (account id, content address, etc.).
Two responsibilities live in this package:
AltsCodexBackend— runs theauthorize → callback → get_tokenchain against the DeOAuth server (api.altscodex.com). One method (get_slot_info) and one callback handler (handle_callback) do the whole thing.AltsCodex— server-side helper for the browser-side flow: builds the login URL, generates the CSRFstate, parses the redirect callback query. Use this when you render the login page from Python (e.g. Jinja, Next.js server actions backed by FastAPI) instead of using the JavaScript SDK popup.
Use this SDK when
- Your backend is FastAPI, Starlette, Quart, Sanic, or any other
asyncio-based framework. - You run the OAuth
get_tokenexchange on the server — i.e. anywhereclient_secretis needed. - You issue session cookies, JWTs, or DB users keyed off
SlotInfo.id.
Use the JavaScript SDK instead when
- You need the browser popup flow (
window.open+postMessage). Popups are inherently browser-only and have no Python equivalent — use the JS SDK on the frontend and call this Python SDK from your backend.
Don't use either when
- You only need to display a user's public profile — there's no public
read-only endpoint in this SDK. Issue a session token from
SlotInfo.idand call your own backend.
pip install altscodex-sdkWith FastAPI helpers (optional — only pulls FastAPI into your env):
pip install "altscodex-sdk[fastapi]"For development against this repo:
git clone https://github.com/banstorm/altscodex-sdk-python.git
cd altscodex-sdk-python
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pytest| Item | Range |
|---|---|
| Python | 3.9 – 3.13 |
httpx |
≥ 0.24 |
| FastAPI / Starlette | any version with Request.query_params (≥ 0.95 in pyproject.toml) |
| Operating systems | any platform supported by CPython + httpx |
A minimal FastAPI server that completes a login round-trip.
# server.py — 최소 FastAPI 통합 예제
import os
from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException, Request
from altscodex import (
AltsCodexBackend,
AuthorizeCallbackTimeoutError,
AuthorizeFailedError,
AuthorizeRejectedError,
ShutdownError,
)
sdk = AltsCodexBackend(
client_id=os.environ["ALTSCODEX_CLIENT_ID"],
client_secret=os.environ["ALTSCODEX_CLIENT_SECRET"],
redirect_uri=os.environ["ALTSCODEX_REDIRECT_URI"],
# auth_server_url defaults to https://api.altscodex.com
)
@asynccontextmanager
async def lifespan(_app: FastAPI):
try:
yield
finally:
await sdk.shutdown()
app = FastAPI(lifespan=lifespan)
@app.post("/getinfo")
async def getinfo(request: Request):
"""DeOAuth callback endpoint. Path must match `redirect_uri` exactly."""
return await sdk.handle_callback(request)
@app.post("/login")
async def login(payload: dict):
"""Receive a JWT from the browser SDK, return the user's slot info."""
jwt = payload.get("jwt")
if not jwt:
raise HTTPException(400, "jwt required")
try:
slot = await sdk.get_slot_info(jwt)
except AuthorizeFailedError as err:
status = 401 if err.code == "EXPIRED_TOKEN" else 502
raise HTTPException(status, str(err)) from err
except AuthorizeCallbackTimeoutError as err:
raise HTTPException(408, str(err)) from err
except AuthorizeRejectedError as err:
raise HTTPException(502, str(err)) from err
except ShutdownError as err:
raise HTTPException(503, str(err)) from err
return {"success": True, "user": slot}Run it:
ALTSCODEX_CLIENT_ID=... \
ALTSCODEX_CLIENT_SECRET=... \
ALTSCODEX_REDIRECT_URI=https://yourapp.com/getinfo \
uvicorn server:app --port 3070A more complete version of the same example lives in
examples/fastapi_app.py.
[Browser] [Your Backend] [DeOAuth Server]
| | |
| 1. JS SDK opens popup ----> | (no backend involvement) |
| 2. user logs in <-------- popup on altscodex.com |
| 3. JS SDK receives JWT | |
| 4. POST /login { jwt } ---> | |
| | 5. get_slot_info(jwt) |
| | pre-registers `state` |
| | in pending map |
| | 6. GET /authorize ---------> |
| | (Bearer jwt + state) |
| | <---- success: true -----|
| | |
| | ... DeOAuth fires callback |
| | 7. POST /getinfo <----------|
| | (query: state, code, |
| | success=1) |
| | 8. handle_callback ack 200 |
| | spawns _exchange_code |
| | 9. POST /get_token --------> |
| | (Basic id:secret) |
| | <---- slot info ---------|
| | 10. resolve pending future |
| 11. {user: slotInfo} <----- | |
Key invariants:
- Step 5 happens before step 6 — the pending entry is created before the authorize HTTP request is dispatched, so a callback that arrives faster than the authorize response (it can happen — the DeOAuth server pipes them concurrently) is still routed correctly.
- Steps 6 and 7 use different transports — step 6 is your backend's HTTP client; step 7 is the DeOAuth server calling back into your backend.
- Step 8 is synchronous, step 9 is fire-and-forget — the DeOAuth server gets its 200 OK immediately so its connection doesn't block on step 9.
class AltsCodexBackend:
def __init__(
self,
*,
client_id: str, # required
client_secret: str, # required — held privately
redirect_uri: str, # required, exact match
auth_server_url: str | None = None, # default: https://api.altscodex.com
http_client: httpx.AsyncClient | None = None, # optional, for reuse / testing
) -> None: ...client_secret is stored in a name-mangled attribute
(_AltsCodexBackend__client_secret) and is not exposed on the instance's
public surface. Tests in this repo enforce that no public attribute equals the
configured secret.
If http_client is omitted, the SDK constructs its own httpx.AsyncClient(timeout=30.0)
and closes it on shutdown(). If you pass one, you keep ownership.
Runs the full chain. The current task awaits a future that is resolved when either:
- the DeOAuth server posts a successful callback and
_exchange_codereturns, or - the authorize call fails immediately (returns
success: false), or - the
timeoutelapses without a callback.
slot = await sdk.get_slot_info(jwt, timeout=20.0)
print(slot["id"], slot["content_address"])Raises one of the exceptions below.
Receives the DeOAuth callback. Returns {"received": True} immediately;
the get_token exchange runs as a background asyncio.Task and resolves
the matching pending future.
@app.post("/getinfo")
async def callback(request: Request):
return await sdk.handle_callback(request)request may be:
- a Starlette/FastAPI
Request(anything exposing.query_params), or - a plain mapping shaped like
{"query": {...}}(useful for tests or non-FastAPI frameworks that wrap the query string differently).
The DeOAuth server sends callbacks via POST. If you accidentally
register it as GET, FastAPI's automatic 405 will fire and your pending
request will time out.
- Marks the SDK as shut down — further
get_slot_infocalls raiseShutdownError. - Cancels every pending future's timeout handle and rejects each one with
ShutdownError. - Closes the owned
httpx.AsyncClient(only if the SDK constructed it).
Always call this from your FastAPI lifespan or shutdown hook. Without
it, in-flight requests leak futures and timers when your worker exits.
class AltsCodex:
def __init__(
self,
*,
client_id: str, # required
redirect_uri: str, # required
altscodex_url: str | None = None, # default: https://altscodex.com
response_type: str = "code",
) -> None: ...
def build_login_url(self, *, state: str | None = None) -> LoginUrl: ...
@staticmethod
def parse_callback(query: Mapping[str, str]) -> CallbackPayload: ...
@staticmethod
def generate_state() -> str: ...The browser popup flow (window.open, postMessage, localStorage) has
no direct Python analogue. This helper handles the two pieces that do
translate:
Build the URL the browser should be redirected to. LoginUrl has .url
and .state attributes and is iterable, so unpacking works:
url, state = helper.build_login_url()
request.session["altscodex_state"] = state # store for CSRF check
return RedirectResponse(url)Parse a server-side redirect callback. CallbackPayload exposes
success: bool, code: str | None, state: str | None, and raw: dict.
payload = AltsCodex.parse_callback(dict(request.query_params))
if payload.state != request.session.get("altscodex_state"):
raise HTTPException(403, "state mismatch")
if not payload.success:
raise HTTPException(401, "login failed")
# Use payload.code to exchange for slot info via AltsCodexBackend._exchange_codeReturns a secrets.token_urlsafe(24) string (~192 bits of entropy).
Suitable for CSRF protection.
class SlotInfo(TypedDict, total=False):
id: str | None # stable user identifier — use as your primary key
access_token: str | None # DeOAuth access token (NOT your session token)
content_address: str | None # on-chain wallet address
token_nickname: str | None # slot nickname chosen by the user
tr_cnt: int | None # transfer count (on-chain activity counter)
code: str | None # the OAuth code that was just exchangedAll fields are typed as Optional because the upstream server can omit any
of them. id and access_token are present in normal cases. Treat
anything you depend on as nullable until you've verified it server-side.
AltsCodexError # base — catch this if you want everything
├── AltsCodexHTTPError # non-2xx from DeOAuth (.status, .payload)
├── AuthorizeCallbackTimeoutError # callback did not arrive in time
├── AuthorizeFailedError # /authorize returned success=false (.code)
├── AuthorizeRejectedError # callback arrived with success != "1"
└── ShutdownError # SDK shut down with this request pending
AuthorizeFailedError.code carries the server-provided code string (e.g.
"EXPIRED_TOKEN", "AUTHORIZE_ERROR") so you can distinguish 401-class
from 502-class failures.
AltsCodexHTTPError.status is the HTTP status code returned by the DeOAuth
server, and .payload is the parsed JSON body (or {"raw": text} if the
body was not JSON).
Recommended HTTP-status mapping for your own endpoints:
| Exception | HTTP |
|---|---|
AuthorizeFailedError (code == "EXPIRED_TOKEN") |
401 |
AuthorizeFailedError (other codes) |
502 |
AuthorizeCallbackTimeoutError |
408 |
AuthorizeRejectedError |
502 |
ShutdownError |
503 |
AltsCodexHTTPError |
mirror .status |
AltsCodexError (catch-all) |
500 |
One AltsCodexBackend per process. Construct at startup, shut down at exit:
@asynccontextmanager
async def lifespan(app: FastAPI):
app.state.sdk = AltsCodexBackend(
client_id=settings.client_id,
client_secret=settings.client_secret,
redirect_uri=settings.redirect_uri,
)
try:
yield
finally:
await app.state.sdk.shutdown()
app = FastAPI(lifespan=lifespan)
def get_sdk(request: Request) -> AltsCodexBackend:
return request.app.state.sdk
@app.post("/login")
async def login(payload: dict, sdk: AltsCodexBackend = Depends(get_sdk)):
return await sdk.get_slot_info(payload["jwt"])If you can't use the JS popup (e.g. native mobile webview), do a plain redirect flow:
helper = AltsCodex(
client_id=settings.client_id,
redirect_uri="https://yourapp.com/auth/callback",
)
@app.get("/auth/start")
async def start(request: Request):
login = helper.build_login_url()
request.session["altscodex_state"] = login.state
return RedirectResponse(login.url)
@app.get("/auth/callback")
async def callback(request: Request, sdk: AltsCodexBackend = Depends(get_sdk)):
payload = AltsCodex.parse_callback(dict(request.query_params))
if payload.state != request.session.pop("altscodex_state", None):
raise HTTPException(403, "state mismatch")
if not payload.success or not payload.code:
raise HTTPException(401, "login failed")
# Note: this path is the redirect-flow shortcut. The standard JWT
# path through sdk.get_slot_info / sdk.handle_callback still works
# in parallel — pick one model per route.The DeOAuth server's default callback transport is POST (the JS popup case). The pure redirect flow above uses GET and is supported only if your client is configured for it — confirm with the Developer Center before relying on this pattern in production.
If your app serves multiple AltsCodex client applications, instantiate
one SDK per client and key the instances by client_id:
class SdkRegistry:
def __init__(self) -> None:
self._by_client: dict[str, AltsCodexBackend] = {}
def get(self, client_id: str) -> AltsCodexBackend:
if client_id not in self._by_client:
cfg = load_client_config(client_id) # from your DB
self._by_client[client_id] = AltsCodexBackend(
client_id=client_id,
client_secret=cfg.secret,
redirect_uri=cfg.redirect_uri,
)
return self._by_client[client_id]
async def shutdown(self) -> None:
for sdk in self._by_client.values():
await sdk.shutdown()Each client_secret stays scoped to its own SDK instance and never crosses
tenant boundaries.
- The pending map is
dict[str, _PendingEntry]guarded by anasyncio.Lock. All add / remove operations are awaited inside the lock. - Each pending entry holds an
asyncio.Futureand aloop.call_latertimeout handle. The timeout fires_reject_pending(state, …)if the callback never arrives. - State generation uses
secrets.token_urlsafe(24)— collision-resistant for any realistic concurrency level. - Authorize dispatch is fire-and-forget — the
get_slot_infocoroutine registers the pending entry, schedules_dispatch_authorizewithasyncio.create_task, and awaits the future. This is what lets a fast callback resolve the future before the authorize response returns. handle_callbackreturns synchronously; theget_tokenexchange runs in a separate task. If your event loop is shutting down while this task is in flight,shutdown()will reject the corresponding pending future withShutdownError.- The SDK is safe under multiple concurrent
get_slot_infocalls with the same JWT or different JWTs. Each call gets its ownstateand its own pending entry. The state-keyed map prevents any cross-talk. - The SDK is not safe across processes — pending state is in-memory.
If you run multiple uvicorn workers and the callback arrives at a
different worker than the one that initiated
authorize, the callback silently no-ops and the originating request will time out. Pin to one worker, use sticky sessions, or move pending state to Redis (see Extending below).
The SDK is intentionally small; for advanced needs subclass or compose:
- Cross-worker pending state — override
_resolve_pending/_reject_pending/ the_pending_by_statestorage to back it with Redis pub/sub. State is a 32-byte URL-safe string; treat it as the Redis key. - Custom retry / proxy / TLS — pass
http_client=httpx.AsyncClient(...). Anythinghttpxsupports works (proxies, custom CAs, HTTP/2, etc.). - Custom telemetry — wrap
get_slot_infoin a decorator that records timing and exception classes. The SDK doesn't ship its own metrics by design.
The SDK reads no environment variables. All configuration is passed
explicitly to the constructor. This keeps secrets out of os.environ
leaks and makes per-request configuration possible (multi-tenant).
A typical environment-variable layout for production deployments:
# .env (server side only — never expose to the browser)
ALTSCODEX_AUTH_SERVER_URL=https://api.altscodex.com
ALTSCODEX_CLIENT_ID=your-registered-client-id
ALTSCODEX_CLIENT_SECRET=your-client-secret
ALTSCODEX_REDIRECT_URI=https://yourapp.com/getinfosdk = AltsCodexBackend(
auth_server_url=os.environ.get("ALTSCODEX_AUTH_SERVER_URL"),
client_id=os.environ["ALTSCODEX_CLIENT_ID"],
client_secret=os.environ["ALTSCODEX_CLIENT_SECRET"],
redirect_uri=os.environ["ALTSCODEX_REDIRECT_URI"],
)For Pydantic settings users:
from pydantic_settings import BaseSettings
class AltsCodexSettings(BaseSettings):
auth_server_url: str = "https://api.altscodex.com"
client_id: str
client_secret: str
redirect_uri: str
model_config = {"env_prefix": "ALTSCODEX_"}
settings = AltsCodexSettings()
sdk = AltsCodexBackend(**settings.model_dump())| Knob | Default | When to change |
|---|---|---|
get_slot_info(..., timeout=...) |
15.0 seconds |
Increase for slow networks; decrease if you want faster failure |
httpx.AsyncClient(timeout=...) |
30.0 seconds |
Pass http_client=... with a custom timeout if needed |
auth_server_url |
https://api.altscodex.com |
Override for staging / local DeOAuth server |
A complete error handler:
from altscodex import (
AltsCodexError,
AltsCodexHTTPError,
AuthorizeCallbackTimeoutError,
AuthorizeFailedError,
AuthorizeRejectedError,
ShutdownError,
)
@app.exception_handler(AltsCodexError)
async def altscodex_exception_handler(_request, exc: AltsCodexError):
if isinstance(exc, AuthorizeFailedError):
status = 401 if exc.code == "EXPIRED_TOKEN" else 502
elif isinstance(exc, AuthorizeCallbackTimeoutError):
status = 408
elif isinstance(exc, AuthorizeRejectedError):
status = 502
elif isinstance(exc, ShutdownError):
status = 503
elif isinstance(exc, AltsCodexHTTPError):
status = exc.status
else:
status = 500
return JSONResponse({"error": str(exc)}, status_code=status)Register this once and your route handlers can call sdk.get_slot_info
without per-route try/except.
The SDK is designed to be testable without a real DeOAuth server. Inject
an httpx.AsyncClient backed by MockTransport:
import json
import httpx
import pytest
from altscodex import AltsCodexBackend
@pytest.fixture
async def sdk():
def handler(request: httpx.Request) -> httpx.Response:
if "/authorize" in request.url.path:
return httpx.Response(
200,
content=json.dumps({"success": True}).encode(),
headers={"content-type": "application/json"},
)
if "/get_token" in request.url.path:
return httpx.Response(
200,
content=json.dumps({"id": "u-1", "access_token": "t"}).encode(),
headers={"content-type": "application/json"},
)
return httpx.Response(404)
client = httpx.AsyncClient(transport=httpx.MockTransport(handler))
instance = AltsCodexBackend(
client_id="cid",
client_secret="cs",
redirect_uri="http://localhost/cb",
http_client=client,
)
yield instance
await instance.shutdown()
@pytest.mark.asyncio
async def test_login_succeeds(sdk):
import asyncio
task = asyncio.create_task(sdk.get_slot_info("jwt"))
await asyncio.sleep(0.05)
state = next(iter(sdk._pending_by_state)) # peek the state for the test
await sdk.handle_callback({"query": {"success": "1", "code": "c", "state": state}})
result = await task
assert result["id"] == "u-1"The repository's own tests/test_backend.py is a
full reference — it ports all six Jest scenarios from the JavaScript SDK
plus contract tests for the secret-leak guarantee and the unknown-state
no-op.
For real end-to-end tests, point auth_server_url at a local DeOAuth
server (the AltsCodex platform exposes a docker-compose target — see the
Developer Center). Don't run E2E tests against the production DeOAuth
server; you'll burn tokens.
Two things to override:
sdk = AltsCodexBackend(
auth_server_url="http://localhost:3000",
client_id="your-local-client-id",
client_secret="your-local-client-secret",
redirect_uri="http://localhost:3070/getinfo",
)Local DeOAuth server requirements:
client_id/client_secretregistered in the local Developer Center.redirect_uriregistered exactly — including protocol, port, and trailing slash (or absence thereof).- CORS allowed from your frontend dev origin if you're using the JS SDK popup.
Running this repo's tests:
pip install -e ".[dev]"
pytest -v- Never put
client_secretin frontend code. It's only valid in server-side environments whereAltsCodexBackendlives. - The SDK stores
client_secretin_AltsCodexBackend__client_secret(name-mangled) so it doesn't appear ondir(instance)or get accidentally serialised by frameworks that pickle/dump public attributes. A unit test in this repo enforces that no public attribute matches the configured secret. redirect_urimust match the Developer Center registration exactly. Including protocol, host, port, path, and trailing slash. Mismatches surface asinvalid_client/redirect_uri mismatch401 errors.- CSRF
stateis mandatory for the redirect flow. The popup flow delivers the JWT viapostMessageto the same origin and doesn't need CSRF, but the redirect flow does — storestatein the user session and compare on callback. - Validate
SlotInfo.idbefore you trust it. Use it as a stable user identifier in your DB, but don't echo it back unhashed in URLs if your threat model includes enumeration. - Don't log JWTs or
access_tokenvalues. They grant DeOAuth-level access for their lifetime.
| Purpose | Production | Local |
|---|---|---|
| Frontend (platform) | https://altscodex.com (or www.) |
http://localhost:3000 |
| Backend / API | https://api.altscodex.com |
http://localhost:3000 |
| Developer Center | https://developers.altscodex.com |
— |
Do NOT invent subdomains like login.altscodex.com, oauth.altscodex.com,
auth.altscodex.com. They resolve to NXDOMAIN and the request silently
fails with User closed the login window / authorize callback timeout.
The first deploy to staging or production is the most common time to hit
this. Register every variant of redirect_uri you actually use (HTTP vs
HTTPS, with vs without trailing slash, every preview-URL host).
The DeOAuth server posts to the registered callback. If you wire it as
@app.get instead of @app.post, FastAPI returns 405 and your pending
request times out. There is no separate error code — diagnose by checking
your access log for a 405 on the callback path.
Without it, pending futures and timers leak when your worker exits, and
the httpx.AsyncClient stays open. In dev this manifests as
ResourceWarning: unclosed client on shutdown. In production it manifests
as a slow exit that times out the worker reaper.
This SDK is fully async. Calling get_slot_info(...) without awaiting it
returns a coroutine object, not a result. If you call it from sync code,
wrap with asyncio.run(...) or use anyio.from_thread.run.
The pending map is per-process. If uvicorn spawns workers --workers 4
and the callback hits a different worker than the one that called
authorize, the request times out. For now, run a single worker or pin
sessions; future versions may support Redis-backed state.
If you're migrating an existing @webxcom/sdk integration, the v2.x
platform supports both client SDKs simultaneously (dual-broadcast
postMessage). The Python SDK is v2.x only — there is no @webxcom
Python equivalent. Migrate the backend first, then the frontend.
| Feature | @altscodex/sdk (Node) |
altscodex-sdk (Python) |
|---|---|---|
| Browser popup login | ✅ new AltsCodex().login() |
❌ (use JS SDK; Python serves the JWT-receiving backend) |
localStorage token storage |
✅ | ❌ (Python is server-side; use cookies/JWT) |
| Build login URL (server side) | ❌ (URL is built inside login()) |
✅ AltsCodex().build_login_url() |
| Parse callback query | ❌ (handled inside backend SDK) | ✅ AltsCodex.parse_callback() |
OAuth authorize → get_token chain |
✅ AltsCodexBackend.getSlotInfo() |
✅ AltsCodexBackend.get_slot_info() |
| Express integration | ✅ app.post('/getinfo', sdk.handleCallback) |
✅ await sdk.handle_callback(request) |
| Concurrency-safe pending map | ✅ | ✅ |
| Graceful shutdown | ✅ sdk.shutdown() |
✅ await sdk.shutdown() |
client_secret privacy |
closure | name-mangled attribute (enforced by tests) |
| HTTP client | built-in fetch / http |
httpx.AsyncClient (injectable) |
| Test mocks | jest global.fetch |
httpx.MockTransport |
| Test scenarios | 6 | 6 (1:1 port) + 8 contract checks |
The two SDKs are designed to interoperate. Typical deployment: JS SDK on the frontend, Python SDK on the backend.
This repository is configured for PyPI Trusted Publisher (OIDC) — you don't need a long-lived API token on PyPI for the standard release path.
- Bump
version = "..."inpyproject.toml. - Update
__version__inaltscodex/__init__.pyto match. - Commit:
git commit -am "chore: release vX.Y.Z". - Tag and push:
git tag vX.Y.Z && git push origin main vX.Y.Z. - GitHub Actions builds, runs
twine check, then uploads to PyPI via OIDC.
The workflow file is .github/workflows/publish.yml.
The PyPI Trusted Publisher must be configured (one-time setup) with:
| PyPI form field | Value |
|---|---|
| PyPI project name | altscodex-sdk |
| Owner | banstorm |
| Repository | altscodex-sdk-python |
| Workflow name | publish.yml |
| Environment name | pypi |
The GitHub repo also needs an environment named pypi (Settings →
Environments → New environment).
If OIDC is unavailable (CI down, hotfix from a laptop), use
scripts/publish.sh:
# Build + twine check only (no upload)
scripts/publish.sh --check
# Upload to TestPyPI first (recommended for first release)
scripts/publish.sh --test
pip install --index-url https://test.pypi.org/simple/ altscodex-sdk==X.Y.Z
# Production upload
scripts/publish.shCredentials: set TWINE_USERNAME=__token__ and TWINE_PASSWORD=pypi-...
in your environment, or put them in ~/.pypirc.
- Fork and clone the repo.
python -m venv .venv && source .venv/bin/activate && pip install -e ".[dev]".- Make your change. Add a test for it. Keep tests deterministic — never
depend on wall-clock time without
pytest'stmp_path/ mocked clocks. pytest -v— all tests must pass on Python 3.9 / 3.10 / 3.11 / 3.12 / 3.13.- Match existing style: module-level Korean header comments, type
annotations on every public function, no
Anyin public signatures unless it's intentional. - PR title in conventional commits (
feat:,fix:,refactor:, etc.).
altscodex-sdk-python/
├── altscodex/ # the package itself
│ ├── __init__.py # public API re-exports + version
│ ├── backend.py # AltsCodexBackend
│ ├── frontend.py # AltsCodex (URL builder + callback parser)
│ ├── exceptions.py # AltsCodexError hierarchy
│ └── types.py # SlotInfo TypedDict
├── examples/
│ └── fastapi_app.py # runnable FastAPI integration
├── scripts/
│ └── publish.sh # token-based publishing fallback
├── tests/
│ ├── test_backend.py # 6 Jest scenarios + contract tests
│ └── test_frontend.py # URL builder + callback parser tests
├── .github/workflows/
│ └── publish.yml # OIDC trusted-publisher release
├── pyproject.toml
└── README.md # this file
pytest -v # all tests
pytest tests/test_backend.py -v # backend only
pytest -k "scenario_4" -v # one scenario
pytest --co # collect, don't run| Resource | URL | Description |
|---|---|---|
| Platform | altscodex.com | Sign up, manage Alts, marketplace |
| Developer Center | developers.altscodex.com | API docs, credentials, support |
| DeOAuth API | https://api.altscodex.com |
Backend SDK target |
| Blockchain Explorer | scan.xotown.com | On-chain identity lookup |
| JS SDK | @altscodex/sdk | Browser-side counterpart |
| This repo | github.com/banstorm/altscodex-sdk-python | Source + issues |
MIT — see LICENSE if present, otherwise the MIT license terms
in pyproject.toml apply.