Skip to content

Developer Docs

Everything here reflects the real, live system — every command on this page has been run against production and verified to work exactly as shown.

1. Payment and credit

One fixed tier: $28/month. No metering, no custom amounts — fiatAmount is a fixed server-side constant, never read from your request. See Pricing for the checkout form.

Credit unit: 1 credit = 1 US cent. $28 = 2800 credits.

Billing runs in 6-day cycles, not calendar months. Each cycle costs 560 credits (2800 ÷ 560 = 5 cycles ≈ one $28 payment's worth of runway). The first 560-credit charge happens atomically at the moment your instance successfully starts running — not retroactively at the end of the cycle — so there's no free trial period hiding in the billing math. Every cycle after that is checked and charged automatically, in advance, every 6 days.

A free trial may also be available — new accounts can be granted a one-time 560-credit bonus (enough for exactly one 6-day trial instance) via the signup form on the homepage. This is independently toggleable and isn't always open; if it's closed, /signup returns {"status": "trial_closed"} and checkout (paid) is the reliable path in.

Multi-instance: one AWS account can run up to 4 instances at once, sharing one credit pool. If your account has more than one instance due for a cycle charge at the same time and the shared balance can't cover all of them, the oldest instance is protected first — newest instance(s) are the ones deprovisioned if credit runs short.

Standing balance cap: at most 22,400 credits (2 months × 4 instances) can be held prepaid at once — enforced before any payment is even created, so a rejected top-up never becomes a payment you'd need refunded.

Running out of credit: when a cycle's charge can't be covered, that instance is deprovisioned automatically — the Lightsail instance, its static IP, and its DNS record are all torn down. Top up again via Pricing any time before or after.

2. Setting up and installation

Your AWS account is your login — every authenticated call below is signed with your own AWS credentials (SigV4), the same mechanism the aws CLI already uses. No separate API key or password to manage.

Step 1 — Get an account and credit

Submit the checkout form on Pricing (email + 12-digit AWS account number) and pay. This registers your account (if it doesn't exist yet) and, once the payment settles, credits it automatically — no manual step on your end.

Step 2 — Provision your instance

All authenticated calls need a SigV4-signing tool. awscurl is the simplest option:

bash
pip install awscurl

export AWS_ACCESS_KEY_ID=<your access key>
export AWS_SECRET_ACCESS_KEY=<your secret key>

awscurl --service execute-api --region ap-southeast-1 -X POST "https://api.niuty.com/create-instance"

Returns once the instance is actually running (usually 15-90 seconds):

json
{"status": "creating", "instance_id": "<id>", "livekit_url": "wss://<id>.rt.niuty.com"}

Once your own agent process is up and listening for a job on this instance, you can talk to it directly from your browser — no frontend of your own required yet — via Test Your Agent.

/session is the same underlying flow (audio and video this time, for an agent that also publishes a video track) as a single shareable link instead of a page you paste a token into - not linked from the site nav, and deliberately not search-indexed, since it's meant to be sent directly to one person rather than browsed to. /test-token's response already includes a ready-to-use sessionUrl field (already URL-encoded) - just copy that, no need to build it by hand. Pass expiresInHours to /test-token if you want the link to stay valid longer than the default 1 hour (up to 7 days) - useful for an evergreen demo link rather than a one-off test.

Opening the link connects immediately, full-screen-capable, no paste step - whoever you send it to just sees/hears the agent right away.

Save instance_id — every other call needs it. This call charges 560 credits atomically on success; if you don't have enough credit, it fails cleanly with {"status": "error", "message": "Insufficient credit."} and nothing is charged.

Step 3 — Check status (optional)

bash
# One specific instance
awscurl --service execute-api --region ap-southeast-1 -X GET "https://api.niuty.com/status?instanceId=<instance_id>"

# Account-wide overview - credit balance + every instance you have
awscurl --service execute-api --region ap-southeast-1 -X GET "https://api.niuty.com/status"

If an instance looks unhealthy (status checks failing, unreachable, nothing responding) — reboot it yourself, no need to contact us:

bash
awscurl --service execute-api --region ap-southeast-1 -X POST "https://api.niuty.com/reboot-instance?instanceId=<instance_id>"

Takes about a minute to come back. Rate-limited to one request per instance per 2 minutes — a second call before that window passes returns {"status": "error", ...} with a 429, not a second reboot.

Step 4 — Fetch your credential

bash
awscurl --service execute-api --region ap-southeast-1 -X GET "https://api.niuty.com/fetch-credential?instanceId=<instance_id>" > ~/.niuty/credentials.enc

This is an encrypted envelope (key_id/nonce/ciphertext), not raw LiveKit credentials — only live-lib (below) can decrypt it. ~/.niuty/credentials.enc is the default path every live-lib API in this doc reads from.

Step 5 — Install live-lib

Run this inside the virtualenv you'll actually use for your agent (python3 -m venv .venv && source .venv/bin/activate first, if you haven't already):

bash
mkdir -p ~/.niuty  # if it doesn't already exist

aws codeartifact login --tool pip --domain niuty --domain-owner 355681235310 --repository niuty-python --region ap-southeast-1

# aws codeartifact login always writes the index-url globally (affects every
# venv and your system Python, not just this one) - move it into this venv
# only, then remove the global copy:
pip config --site set global.index-url "$(pip config get global.index-url)"
pip config --user unset global.index-url

pip install "live-lib[livekit]"

Use the [livekit] extra — the base live-lib install is schema-only (for tools that just need to author/export flows) and deliberately doesn't pull in the LiveKit/OpenAI/Google SDK stack.

CodeArtifact's own upstream chain (niuty-pythonpypi-store → public PyPI) means this same index also transparently serves ordinary public packages (boto3, awscurl, etc.) — you never need a second index configured for anything else this venv installs.

Troubleshooting:

  • 401 Error, Credentials not correct on any pip install, even unrelated public packages: the CodeArtifact login token embedded in your pip config has expired — it's only valid 12 hours. Fix: re-run the aws codeartifact login + pip config --site set / pip config --user unset block above (inside the same venv) to get a fresh token.
  • A different, unrelated venv (or your bare system Python) is also trying to hit CodeArtifact, or fails the same way: the index-url leaked into the global/user-level pip config instead of staying scoped to one venv — most often from running plain aws codeartifact login --tool pip ... without the --site/--user follow-up commands above. Fix: pip config --user unset global.index-url (removes it everywhere except whichever venvs already have their own --site-scoped copy), then redo the Step 5 block above inside whichever venv(s) actually need it.

If codeartifact login fails with AccessDeniedException on GetAuthorizationToken: your AWS account is already granted access at the resource-policy level (that happens automatically when your account registers), but if you're calling as a specific IAM user rather than the account root, that user also needs their own identity-based policy allowing it — a resource policy alone isn't sufficient for a non-root caller. Attach this to the IAM user you're using:

json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "codeartifact:GetAuthorizationToken",
        "codeartifact:GetRepositoryEndpoint",
        "codeartifact:ReadFromRepository"
      ],
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": "sts:GetServiceBearerToken",
      "Resource": "*",
      "Condition": { "StringEquals": { "sts:AWSServiceName": "codeartifact.amazonaws.com" } }
    }
  ]
}

(Or simply call codeartifact login with your account's root credentials for this one step — root isn't subject to identity-based policy restrictions at all.)

3. live-lib API usage

This covers the APIs actually used in a real, production live-lib integration — not the full surface. Two independent halves: FlowGraph (design-time — build your agent's conversation logic in Python) and agent_runtime (run-time — connect that logic to a live LiveKit voice session using your niuty-provisioned instance).

FlowGraph — designing your agent's conversation

python
from live_lib import FlowGraph

graph = FlowGraph(name="support_agent", persona="Friendly, concise support rep.")

@graph.node("MAIN_ROUTING", objective="Figure out what the caller needs")
def main_routing(n):
    n.instructions("Greet the caller.", "Ask how you can help.")
    n.on(condition="caller wants billing help", to="BILLING")
    n.on(condition="caller wants technical support", to="SUPPORT")

@graph.node("BILLING", objective="Resolve a billing question")
def billing(n):
    n.instructions("Ask for the account or invoice number.", "Explain the charge or issue a fix.")
    n.on(condition="caller's question is resolved", to="MAIN_ROUTING")
    n.on(condition="caller needs a human", to="SUPPORT")

graph.entry("MAIN_ROUTING")
flow = graph.compile()

Each node is a live conversation state, not a text-generation step — transitions (n.on) are what your caller actually says next, not function calls. Flows are cyclic state machines (see BILLING looping back to MAIN_ROUTING above), not one-way scripts. See FlowGraph on the homepage for the full visual walkthrough.

LiveKitBridgeWorker — connecting a compiled flow to a live session

python
from live_lib.agent_runtime import LiveKitBridgeWorker

worker = LiveKitBridgeWorker(
    openai_api_key="<your OpenAI key>",
    google_api_key="",              # optional, only if using Google's models
    agent_name="my-agent-bridge",   # must match your dispatch/room-naming setup
    room_id_prefix="agent-",
)
worker.start()

livekit_url/api_key/api_secret are never constructor parameters — the worker reads them itself, internally, by decrypting ~/.niuty/credentials.enc (or a custom path via credential_path=). There's no other way to supply them; a missing or invalid credential file raises CredentialError and the worker never starts. This is deliberate: it means a real LiveKit connection is only ever possible using an instance you've actually provisioned and paid for through this system, not an ad-hoc URL/key pasted into code.

After construction, worker.livekit_url / worker.api_key are readable (useful for logging) even though they were never passed in.

LiveKitAgentMixin — wiring a flow into your own agent class

python
from live_lib.agent_runtime import LiveKitAgentMixin

class MyAgent(MyBaseAgentClass, LiveKitAgentMixin):
    ...

Mix this into your existing agent class to pick up the LiveKit-side integration (room registration, dispatch pairing) without restructuring your own agent hierarchy.

function_tool / find_function_tools — exposing Python methods as LLM tools

python
from live_lib.agent_runtime.tools import function_tool, find_function_tools

class MyToolProvider:
    @function_tool(description="Forward a complex query to a backing service.")
    async def ask_backend(self, query: str) -> str:
        return await self._bridge.forward_to_agent(query)

# Later, wherever your worker/agent expects a tool list:
tools = find_function_tools(MyToolProvider())

@function_tool marks a method as LLM-callable with a description the model uses to decide when to call it; find_function_tools discovers every decorated method on an instance and returns them as a ready-to-use tool list.

4. Minimal working example (copy, paste, run)

Everything above as one complete, runnable script — a tiny FlowGraph wired into LiveKitBridgeWorker, using Gemini. This is the fastest way to have something real to talk to via Test Your Agent: save it, fill in your Gemini API key, run it, then connect from that page using the same room name.

python
import time

from live_lib import FlowGraph
from live_lib.agent_runtime import LiveKitBridgeWorker

# 1. Design the conversation. A single looping node is enough for a first test -
# see "FlowGraph" above for branching to multiple nodes.
graph = FlowGraph(name="minimal_greeter", persona="Friendly, upbeat voice assistant.")

@graph.node("GREETING", objective="Greet the caller and see how you can help")
def greeting(n):
    n.instructions("Greet the caller warmly.", "Ask what they'd like help with today.")
    n.on(condition="caller says anything", to="GREETING")

graph.entry("GREETING")
flow = graph.compile()


# 2. Wire the compiled flow into an agent class. LiveKitBridgeWorker reads these
# attributes via getattr - there's no import/registration step beyond this.
# get_livekit_tools is optional (no function tools needed for a minimal
# example) - defining it as a no-op just avoids a harmless but confusing
# "object has no attribute 'get_livekit_tools'" warning in your logs.
class MinimalAgent:
    LLM_PROVIDER = "gemini"
    VOICE = "Puck"
    SYSTEM_FLOW = flow

    def get_livekit_tools(self, bridge):
        return []


AGENT_ID = "demo"
ROOM_NAME = "demo-room"  # must match the roomName you pass to /test-token

# 3. livekit_url/api_key/api_secret come from ~/.niuty/credentials.enc
# (Step 4 above) - never pass them directly.
worker = LiveKitBridgeWorker(
    openai_api_key="",  # unused for this Gemini-only example
    google_api_key="<your Gemini API key>",
    agent_name="minimal-test-agent",
)

# 4. Register the agent, then map the room name directly with notify_join -
# _entrypoint's first (and fastest) pairing check is exactly this mapping,
# so this makes pairing instant. Without it, LiveKitBridgeWorker instead
# spends up to 2 real minutes retrying prefix-based matching (default
# room_id_prefix="agent-", which "demo-room" doesn't start with) before
# falling back to the single-registered-agent path - confirmed live: every
# first connection sat silent for ~120s before the agent ever responded.
worker.register(AGENT_ID, agent_ref=MinimalAgent())
worker.notify_join(AGENT_ID, ROOM_NAME)
worker.start()
time.sleep(3)  # give the worker a moment to register with LiveKit before dispatching

# 5. Jobs are only ever explicitly dispatched, never automatic on room join -
# this must be the SAME room name you pass as `roomName` to /test-token.
worker.create_agent_dispatch(ROOM_NAME)

print(f"Agent running, dispatched to room '{ROOM_NAME}'. Ctrl+C to stop.")
while True:
    time.sleep(3600)

Requires pip install "live-lib[livekit]" (Step 5 above) and a Gemini API key. Leave the process running, then go to Test Your Agent and request a token for roomName=demo-room — you should hear MinimalAgent greet you within a few seconds of connecting.

Built on LiveKit. Sign in with the AWS account you already have.