Skip to content

Quickstart

Pick a language; the same five operations show up in every tab. If you want to drop HTTP in anywhere, the curl tab at the bottom of each block is the protocol-level reference.

1. Install

bash
pip install stoka-agent
bash
go get github.com/ajbeach2/stoka-go
bash
npm install stoka
bash
# Same package, same install — TypeScript and plain Node share it.
npm install stoka
bash
# Nothing to install. curl + jq + a signed X-PAYMENT header is enough.

2. Get a funded testnet wallet

You need a Stellar testnet seed (S…) with USDC and the USDC trustline. The quickest path is the Circle faucet, which gives you both:

  1. Generate a wallet (Freighter, the SDK, or the make wallet-testnet target in this repo).
  2. Open https://faucet.circle.com/, pick Stellar Testnet, paste the G… address.
  3. Export the seed:
bash
export STELLAR_SECRET=SXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

3. Construct the client

python
import os
from stoka_agent import X402Client

client = X402Client(env="test", secret=os.environ["STELLAR_SECRET"])
go
package main

import (
    "context"
    "os"

    stoka "github.com/ajbeach2/stoka-go"
)

func main() {
    c, err := stoka.New(stoka.Options{
        Env:    stoka.Testnet,
        Secret: os.Getenv("STELLAR_SECRET"),
    })
    if err != nil {
        panic(err)
    }
    defer c.Close()
    _ = context.Background()
}
ts
import { createClient } from "stoka";

const client = createClient({
  env: "test",
  secret: process.env.STELLAR_SECRET!,
});
ts
// Same module; Node just pulls the Node-specific signer under the hood.
import { createClient } from "stoka";

const client = createClient({
  env: "test",
  secret: process.env.STELLAR_SECRET!,
});
bash
# No client object for raw HTTP. Set these once and the rest of the
# page refers to them.
export STOKA_API=https://test.api.stoka.space

4. Store a blob

python
client.store("greeting", b"remember this tomorrow", ttl_seconds=7 * 86400)
go
ctx := context.Background()
_, err := c.Store(ctx, "greeting", []byte("remember this tomorrow"), &stoka.StoreOptions{
    TTLSeconds: 7 * 86400,
})
ts
await client.store(
  "greeting",
  new TextEncoder().encode("remember this tomorrow"),
  { ttlSeconds: 7 * 86400 },
);
ts
import { readFileSync } from "node:fs";

await client.store("report.pdf", readFileSync("./report.pdf"), {
  ttlSeconds: 7 * 86400,
});
bash
# The paid flow is a two-request dance: the first call comes back 402
# with payment requirements, the second resends the same request with
# a signed X-PAYMENT header. The clients do this for you; curl cannot.
# See /docs/guide/x402 for the wire format if you want to build your own.
curl -i -X POST "$STOKA_API/v1/store" \
  -H "X-Stoka-Key: greeting" \
  -H "X-Stoka-TTL-Seconds: 604800" \
  -H "Content-Type: application/octet-stream" \
  --data-binary "remember this tomorrow"
# → HTTP/1.1 402 Payment Required
# → { "x402Version": 2, "accepts": [ { "maxAmountRequired": "...", ... } ] }

5. Retrieve a blob

python
body = client.retrieve("greeting")
print(body)                             # bytes, auto-decrypted
go
body, err := c.Retrieve(ctx, "greeting", nil)
if err != nil {
    panic(err)
}
fmt.Println(string(body))
ts
const res = await client.retrieve("greeting");
console.log(new TextDecoder().decode(res.bytes));
ts
import { writeFileSync } from "node:fs";

const { bytes } = await client.retrieve("report.pdf");
writeFileSync("./out.pdf", bytes);
bash
curl -i "$STOKA_API/v1/retrieve/greeting" \
  -H "X-Stoka-Owner: G..."               # optional: pins the 402 price

6. Update (overwrite)

python
client.update("greeting", b"newer bytes")
go
if _, err := c.Update(ctx, "greeting", []byte("newer bytes"), nil); err != nil {
    panic(err)
}
ts
await client.update("greeting", new TextEncoder().encode("newer bytes"));
ts
await client.update("greeting", new TextEncoder().encode("newer bytes"));
bash
curl -i -X PUT "$STOKA_API/v1/object/greeting" \
  -H "Content-Type: application/octet-stream" \
  --data-binary "newer bytes"
# Same 402 → sign → retry flow as store.

7. Delete (free, wallet-signed)

python
from stoka_agent import IdentityClient

IdentityClient(env="test", secret=os.environ["STELLAR_SECRET"]).delete("greeting")
go
// Delete is not yet wired into the Go client; use DELETE /v1/object/<k>
// directly with an Authorization: Stellar <addr>:<cid>:<sig_hex> header,
// or use the Python/TS client.
ts
await client.delete("greeting");
ts
await client.delete("greeting");
bash
# 1. Ask for a challenge.
curl -s -X POST "$STOKA_API/v1/auth/challenge" \
  -H "Content-Type: application/json" \
  -d '{"audience":"object-delete"}'
# 2. Sign the `message` with your wallet's Ed25519 key (hex sig).
# 3. Send the delete.
curl -i -X DELETE "$STOKA_API/v1/object/greeting" \
  -H "Authorization: Stellar GABC...:<challenge_id>:<hex_signature>"

8. Handle payment errors

The clients throw a typed PaymentRequired when they hit a 402 they can't satisfy (no signer, unfunded wallet, missing trustline, unknown asset). Everything else lands as StokaError (or the language's standard HTTP-error equivalent).

python
from stoka_agent import PaymentRequired, StokaError

try:
    client.store("greeting", b"...")
except PaymentRequired as e:
    # e.requirements carries price / asset / pay_to.
    print("fund", e.requirements.pay_to, "with", e.requirements.max_amount_required)
except StokaError as e:
    print("HTTP", e.status, e.body)
go
import "errors"

var (
    pr *stoka.PaymentRequired
    se *stoka.StokaError
)

switch {
case errors.As(err, &pr):
    fmt.Printf("fund %s with %s %s\n",
        c.PublicKey(), pr.Requirements.MaxAmountRequired, pr.Requirements.Asset)
case errors.As(err, &se):
    fmt.Printf("HTTP %d: %s\n", se.Status, se.Body)
}
ts
import { StokaError } from "stoka";

try {
  await client.store("greeting", new Uint8Array([0x68, 0x69]));
} catch (e) {
  if (e instanceof StokaError) {
    console.error("HTTP", e.status, e.body);
  } else {
    console.error(e);   // Soroban / network
  }
}
ts
import { StokaError } from "stoka";

try {
  await client.store("greeting", new Uint8Array([0x68, 0x69]));
} catch (e) {
  if (e instanceof StokaError) {
    console.error("HTTP", e.status, e.body);
  } else {
    console.error(e);
  }
}
bash
# Non-2xx responses return { "error": "…" } as JSON. A second 402 after
# you already sent X-PAYMENT means the payment couldn't settle — the
# body describes why (insufficient funds, missing trustline, etc).

Where next?

MIT Licensed.