Building on YieldFabric — single-read entry for LLMs
Wire-level reference for LLMs (and humans) building apps on YieldFabric. Auth flows, the five most-common operations, error handling, common pitfalls. Pragmatic companion to the alignment-economy thesis.
You are an LLM tasked with building an app on top of YieldFabric. This file is the wire-level reference — auth flows, endpoints, mutation shapes, error handling, common pitfalls. Pragmatic and HTTP-shaped. Read it once; then build.
Read the alignment-economy guide first if you haven't. It's the lens for why the platform is shaped the way it is — the five operational roles (reason → communicate → negotiate → structure → execute), what role AI plays in each, and why the Intent → Confirm boundary is the most important architectural element in any application you build on YF. This file tells you how to call the API; that guide tells you what you're calling it for.
What YieldFabric is
YieldFabric is infrastructure for the alignment economy — a substrate where humans align on new agreements through AI-augmented conversation, with the moment of agreement performed by the humans (via the Intent → Confirm boundary) and AI doing translation, knowledge access, and proposal composition on the edges.
Concretely: confidential on-chain payments and obligations, multi-agent orchestration, working-group collaboration. AI proposes, humans confirm, the system executes. The financial primitives (payments, swaps, repos) and the agentic primitives (workspaces, threads, knowledge graphs) are not two separate stacks — they are the same alignment process at different operational stages.
A YieldFabric app talks to four public hosts:
auth.yieldfabric.compay.test.yieldfabric.com (chain 153) / pay.live.yieldfabric.com (chain 151)default_chain_id.api.yieldfabric.com/graphqlagents.yieldfabric.comThe rule of thumb: gateway for federated/shared-core reads, JWT-selected payments-direct GraphQL for chain-bound mutations and instance-local reads, auth REST for sign-in, the same selected payments host for settlement/events, and agents REST/SSE for conversation state. That's the whole map.
One alignment process at five operational stages. The financial primitives and the agentic primitives are not parallel stacks — they are the same alignment process at different stages (see the alignment-economy guide). Reason (knowledge graphs, agentic pipelines) and communicate (working groups, threads, agents-as-edge-translators) and negotiate (intents, drafts, Studio) live primarily in the agents service; structure (contracts, swaps, repos) and execute (payments, settlement) live primarily in the payments service. The Intent → Confirm boundary is the structural locus where wanting collapses into a definite joint prompt — the most important UI element in any application you build.
Before wiring the financial-side endpoints below, read
/docs/guides/agents-and-workspaces
for the reason/communicate/negotiate side. That's where most
applications spend most of their time.
The URLs your app calls — and the ones it shouldn't
A consumer app uses these public surfaces, and nothing else:
POST https://auth.yieldfabric.com/auth/** (REST)/auth/login, /auth/signature/signin, /auth/<provider>/exchange), token refresh, GET /protected/jwt to resolve the caller, and invitations (/auth/invitations/** — create / list / accept; GET /auth/invitations/{token} is public for the magic-link landing).POST https://api.yieldfabric.com/graphql (GraphQL gateway)POST <selected-payments-host>/graphqlpay.test for JWT chain 153 and pay.live for chain 151.GET <selected-payments-host>/api/users/{user_id}/messages/{message_id}executed is non-null. Use the same host as the mutation.https://agents.yieldfabric.com/** (agents REST/SSE)Do not route payments work through raw service-to-service endpoints. Use the corresponding payments-direct GraphQL mutation, then poll the message-status endpoint on that same instance.
<payments-host>/api/mq/submit (raw queue submission)403 Forbidden — Insufficient permissions: CryptoOperations otherwise). Use payments-direct GraphQL mutations instead.<payments-host>/api/chain/** (chain reads)agents.yieldfabric.com/api/mcp/** (MCP transport)Authenticate in 30 seconds
Four sign-in paths. Browser clients use email/password, wallet signature, or provider exchange (#1, #2, #4). Backend services (app backends, scheduled workers, integration scripts) use API keys (#3).
1. Email + password (easiest)
LOGIN=$(curl -s -X POST https://auth.yieldfabric.com/auth/login \
-H 'Content-Type: application/json' \
-d '{"email":"user@example.com","password":"…"}')
TOKEN=$(echo "$LOGIN" | jq -r .token)
REFRESH=$(echo "$LOGIN" | jq -r .refresh_token)
USER_ID=$(echo "$LOGIN" | jq -r .user.id)
Access tokens last 15 min. Refresh tokens last 30 days, single-use, rotated on every refresh.
2. Wallet signature (for wallet-led users)
# 1. Nonce
NONCE=$(curl -s https://auth.yieldfabric.com/auth/signature/nonce | jq -r .nonce)
# 2. Wallet signs $NONCE off-chain (EIP-191 personal_sign)
# 3. Exchange
curl -X POST https://auth.yieldfabric.com/auth/signature/signin \
-H 'Content-Type: application/json' \
-d "{\"public_key\":\"0x04…\",\"signature\":\"0x…\",\"message\":\"$NONCE\",\"nonce\":\"$NONCE\"}"
3. API key (for backend services — what your server-side code should use)
For non-browser callers — your app's backend, scheduled workers, batch jobs, anything that doesn't log a human in at boot — don't store an email + password in env vars. Instead:
- Authenticate once, manually, as the service-account user with any of the user-facing flows (email/password is fine here — it's one-time).
- Mint a long-lived API key for that user:
The rawcurl -X POST https://auth.yieldfabric.com/auth/api-key/generate \ -H "Authorization: Bearer $ONE_TIME_TOKEN" \ -H 'Content-Type: application/json' \ -d '{"service_name":"my-app-backend","description":"Application backend"}' # → { "api_key": "yf_api_…", "service_name": "…", … }yf_api_…value is returned only at creation time; the server stores its hash only. Save it; store it asAPI_KEYin your service's env. - At runtime, exchange the API key for a short-lived JWT on every
boot and on every refresh:
curl -X POST https://auth.yieldfabric.com/auth/api-key \ -H 'Content-Type: application/json' \ -d '{"api_key":"yf_api_…"}' # → same AuthResponse shape as /auth/login
Why this matters: API keys can be revoked individually
(POST /auth/api-keys/{key_id}/revoke) and audited
(GET /auth/api-keys) without rotating the underlying user's
password. They're also the canonical answer to "how does my server-side
code authenticate" — this is the recommended pattern for any
non-browser service that needs to call YF.
Full reference: the API-keys section of the auth reference.
4. Provider exchange (Averer / MetaMask / Email / WebAuthn)
# Discover available providers in this deployment
curl https://auth.yieldfabric.com/auth/providers/config
# Per-provider exchange — see docs at /docs/api/auth (on the running website)
curl -X POST https://auth.yieldfabric.com/auth/metamask/exchange \
-H 'Content-Type: application/json' \
-d '{"address":"0x…","signature":"0x…","message":"<contains nonce>","chain_id":1}'
All four token exchange paths return the same LoginResponse shape:
{
"token": "<access JWT, 15min>",
"refresh_token": "<refresh, 30d, single-use>",
"token_type": "Bearer",
"expires_in": 900,
"user": { "id": "…", "email": "…", "role": "…" }
}
Live/Test mode is a JWT routing claim
Production login starts on chain 153 (Test). The browser reads the signed
JWT's default_chain_id only to select a configured payments URL; the chosen
payments instance still validates the token and rejects writes whose claim
does not match its pinned chain.
Switching mode is an explicit two-step ceremony:
POST /entities/{user|group}/{id}/default-chain {"chain_id":"151"}activates or deploys the target-chain account.POST /auth/refresh {"refresh_token":"…","chain_id":"151"}rotates the session so every direct payment transport reconnects to mainnet.
The first-party app configures both hosts but separately declares which
chains are enabled. Until mainnet contracts are deployed, only 153 is
enabled and Live is visibly unavailable. Enabling chain 151 and the
payments-dual deployment profile unlocks Live without a code change.
When acting as a group, the ceremony activates both the personal and group
wallets and then re-mints the delegation JWT on the same target chain.
Make your first authenticated request
Use the JWT as Authorization: Bearer <token> on any public
surface. Same JWT works for auth REST, the GraphQL gateway, payments
status polling, and agents REST.
# Auth: get current user
curl https://auth.yieldfabric.com/auth/users/me -H "Authorization: Bearer $TOKEN"
# Gateway GraphQL: list payments visible to your entity
curl -X POST https://api.yieldfabric.com/graphql \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"query":"query { paymentsByEntity { id status amount assetId } }"}'
# Agents: list working groups
curl https://agents.yieldfabric.com/working-groups -H "Authorization: Bearer $TOKEN"
The seven things you'll do most
1. Refresh the token
Access tokens expire in 15 min. Rotate before they do:
# Resolve the active mode from the authenticated session. Browser clients read
# the same signed `default_chain_id` claim as a routing hint.
CHAIN_ID=$(curl -s https://auth.yieldfabric.com/protected/jwt \
-H "Authorization: Bearer $TOKEN" | jq -r .default_chain_id)
case "$CHAIN_ID" in
153) PAYMENTS_BASE=https://pay.test.yieldfabric.com ;;
151) PAYMENTS_BASE=https://pay.live.yieldfabric.com ;;
*) echo "Unsupported payment chain: $CHAIN_ID" >&2; exit 1 ;;
esac
PAYMENTS_GRAPHQL="$PAYMENTS_BASE/graphql"
NEW=$(curl -s -X POST https://auth.yieldfabric.com/auth/refresh \
-H 'Content-Type: application/json' \
-d "{\"refresh_token\":\"$REFRESH\",\"chain_id\":\"$CHAIN_ID\"}")
TOKEN=$(echo "$NEW" | jq -r .access_token)
REFRESH=$(echo "$NEW" | jq -r .refresh_token)
CHAIN_ID is the current JWT's default_chain_id; sending a different
served chain is the second half of the explicit mode-switch ceremony,
after that entity's target-chain wallet has been activated. Refresh tokens
are single-use. If you get a 401 here, the user must re-sign-in.
2. Send a payment
curl -X POST "$PAYMENTS_GRAPHQL" \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"query": "mutation($input: InstantSendInput!) { instant(input: $input) { success messageId paymentId } }",
"variables": { "input": {
"destinationWalletId": "<recipient wallet UUID>",
"assetId": "aud-token-asset",
"amount": "10",
"idempotencyKey": "<fresh UUIDv4>"
}}
}'
destinationWalletId is the canonical recipient identifier when you
have a wallet UUID. If you only have a name or email, use destinationId
as an entity-name lookup. Do not pass a raw 0x… address to
destinationId; resolve the wallet first and pass destinationWalletId.
The full input shape (incl. destinationWalletId,
contractId, requireManualSignature, …) lives in
the GraphQL schema (browseable in /docs/api/payments)
under input InstantSendInput.
Idempotency key rule — omit the field and let the resolver
generate one, or pass a fresh UUIDv4 for each submission. Same key +
same body = same response (no double-charge); same key + different
body = 400 IDEMPOTENCY_CONFLICT. Never use a deterministic key.
The recipient must accept incoming payments. A confidential
payment lands as pending in the recipient's wallet — it isn't
spendable until accepted. Accept on demand with acceptAll, or arm a
wallet so a background worker accepts its incoming automatically (one
asset, a set, or all). Full developer guide:
Auto-accept incoming payments.
3. Mint and accept an obligation (zero-payment agreement flow)
An Obligation is a counterparty agreement that can carry payments or be a pure agreement (T&C, attestation, role acceptance). Mint from the admin / issuer side, accept from the counterparty side. Both go through the JWT-selected payments-direct GraphQL endpoint.
# 1. Mint — admin's Bearer. The resolver derives the minter's account
# from the JWT, picks the obligation contract address from the
# chain registry, generates the on-chain token id, AND auto-generates
# a fresh idempotency_key. You only supply the counterpart and the
# off-chain `data` blob.
CREATE_RESP=$(curl -s -X POST "$PAYMENTS_GRAPHQL" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"query": "mutation($input: CreateObligationInput!) { createObligation(input: $input) { success contractId obligationResult messageId message } }",
"variables": { "input": {
"counterpartWalletId": "<UUID from /protected/jwt default_wallet_id>",
"data": { "kind": "tnc", "version": "v1.0", "title": "...", "body_sha256": "..." }
}}
}')
# → { "createObligation": { "success": true, "contractId": "CONTRACT-OBLIGATION-...",
# "obligationResult": null, "messageId": "<uuid>", ... } }
# 2. Wait for the on-chain mint to settle. The mutation returns as
# soon as the message is enqueued; the contract isn't readable by
# acceptObligation until the executor finishes. Poll every 2s
# until `executed` is set (matches the YF Python SDK's
# poll_message_completion).
MSG_ID=$(echo "$CREATE_RESP" | jq -r .data.createObligation.messageId)
ADMIN_USER_ID=$(curl -s https://auth.yieldfabric.com/protected/jwt -H "Authorization: Bearer $ADMIN_TOKEN" | jq -r .user_id)
until curl -s -H "Authorization: Bearer $ADMIN_TOKEN" \
"$PAYMENTS_BASE/api/users/$ADMIN_USER_ID/messages/$MSG_ID" \
| jq -e '.executed' > /dev/null; do sleep 2; done
# 3. Accept — counterpart's Bearer (the user). Keys off `contractId`
# from the create response.
CONTRACT_ID=$(echo "$CREATE_RESP" | jq -r .data.createObligation.contractId)
ACCEPT_RESP=$(curl -s -X POST "$PAYMENTS_GRAPHQL" \
-H "Authorization: Bearer $USER_TOKEN" \
-H 'Content-Type: application/json' \
-d "{
\"query\": \"mutation(\$input: AcceptObligationInput!) { acceptObligation(input: \$input) { success message obligationId messageId } }\",
\"variables\": { \"input\": { \"contractId\": \"$CONTRACT_ID\" } }
}")
# 4. Wait for the accept message to settle the same way, this time
# polling under the user's identity since the message belongs to
# their entity.
ACCEPT_MSG_ID=$(echo "$ACCEPT_RESP" | jq -r .data.acceptObligation.messageId)
USER_USER_ID=$(curl -s https://auth.yieldfabric.com/protected/jwt -H "Authorization: Bearer $USER_TOKEN" | jq -r .user_id)
until curl -s -H "Authorization: Bearer $USER_TOKEN" \
"$PAYMENTS_BASE/api/users/$USER_USER_ID/messages/$ACCEPT_MSG_ID" \
| jq -e '.executed' > /dev/null; do sleep 2; done
Things that trip people up — all of these are pitfalls real apps have hit during their first integration:
counterpartis an entity name, not an address. Passing0x…fails with"No entity found with name '0x…'". UsecounterpartWalletIdwhenever you have the wallet UUID (every authenticated caller does —/protected/jwtreturnsdefault_wallet_id). Same caveat applies toobligor/obligorWalletId.- Don't pass a deterministic
idempotencyKeyfrom app code (e.g.sha256(user || version)). The resolver dedupes on it; a retry after a stuck message just returns the stuckmessageIdagain without re-enqueueing. Either omit it (the resolver auto-generates a fresh one from timestamp+user) or use a UUIDv4 per submission. contractId≠obligationResult≠ on-chain id.contractIdis the YF-side identifier; that's whatacceptObligationkeys off. The on-chain BigUint obligation id comes back asobligationResulton create (oftennullpre-settle) and asobligationIdon accept. Save both — the on-chain id is your durable receipt, the contractId is the YF handle.- Zero-payment obligations omit
denomination,obligor/obligorWalletId, andinitialPayments. The resolver treats this as a pure agreement. - The caller minting the obligation needs
CryptoOperationspermission, since the executor's vault retrieves the minter's default key pair from/keys/users/{id}/default-key, which gates on that permission. For users created withrole: SuperAdmin/Admin/Manager, the JWT mint falls back to role-default permissions whenuser_permissionsis empty; for narrower roles (Viewer,ApiClient, …) you need to grantCryptoOperationsexplicitly viaPOST /auth/users/{id}/permissions. - Make an obligation NFT a transferable spend authority. A data
policy can name an obligation NFT
(collection, tokenId)as its executor: whoever currently holds that NFT may run boundedexecuteUnderPolicysends from a group account, and transferring the NFT transfers the right (the property-management "agency credential" pattern). Pass the collection inexecutorAccountsand the non-zero token id inexecutorIdstoaddDataPolicy; the on-chain gate readsownerOflive at every call. See the NFT-holder policy executors guide for the lifecycle, the mandatory amount bound, and the security model.
4. List a wallet's activity (history feed)
Unified, paginated feed merging the wallet's payments and on-chain
messages, newest first. Use this instead of stitching
payments.byWallet and /api/users/.../messages client-side.
WALLET_ID="..." # /protected/jwt returns default_wallet_id
curl -X POST "$PAYMENTS_GRAPHQL" \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"query": "query Activity($walletId: String!, $limit: Int, $cursor: String) {
walletFlow {
activity(walletId: $walletId, limit: $limit, cursor: $cursor) {
nextCursor
items {
__typename
... on PaymentActivity { timestamp payment { id status amount assetId } }
... on MessageActivity { timestamp message { id messageType status executed } }
}
}
}
}",
"variables": { "walletId": "'"$WALLET_ID"'", "limit": 25 }
}'
Pagination contract:
cursoris opaque. Don't parse it. PassnextCursorfrom the previous page back in as the nextcursor.nextCursor: nullmeans the feed is exhausted — stop.limitdefaults to 25, clamped to 200.- The same
(walletId, limit, cursor)triple is guaranteed to return the same page regardless of which replica answers (stateless, no sticky sessions).
The feed is sorted by timestamp DESC across both streams. Within
a microsecond tie, items break by id lexicographically — stable
but arbitrary across __typename. Don't depend on relative order
of items that share a timestamp.
5. Poll for message status
Long-running on-chain operations (deposit / send / swap / create
obligation) submit a message, return a messageId, then
execute asynchronously. Poll:
curl "$PAYMENTS_BASE/api/users/$USER_ID/messages/$MESSAGE_ID" \
-H "Authorization: Bearer $TOKEN"
Status field progression: Pending → Validating → Executing →
Completed (or Failed / ManualSignature). See
MessageStatus enum.
6. Subscribe to live events (SSE)
For real-time UI, open a Server-Sent Events stream rather than polling:
# Working-group chat stream (new messages, agent responses, typing indicators)
curl -N "https://agents.yieldfabric.com/working-groups/$GROUP_ID/chat/stream" \
-H "Authorization: Bearer $TOKEN"
# Working-group workflow stream (step start / complete / fail)
curl -N "https://agents.yieldfabric.com/working-groups/$GROUP_ID/workflows/$WORKFLOW_ID/stream" \
-H "Authorization: Bearer $TOKEN"
# Pipeline events (KG mutations, intent confirmation prompts)
curl -N "https://agents.yieldfabric.com/pipelines/$PIPELINE_RUN_ID/events" \
-H "Authorization: Bearer $TOKEN"
Threads + workflows are scoped to a working group; the SSE routes
follow that nesting. The full event surface is enumerated in
docs/getting-started/webhooks-and-events.md.
Server sends event: <type>\ndata: <json>\n\n frames. Reconnect on
drop; use polling on reconnect to catch up missed state.
7. Invite someone to join (and optionally execute an agreement)
A magic-link invitation onboards a new user and routes them into a typed
action on accept. target_kind is one of none | obligation | deal_flow | group_join | connection; the optional target_payload carries the terms
(e.g. an obligation's amount / denomination / expiry).
# 1. Inviter creates the invitation. The email is sent automatically; the
# response also returns the magic link to share out-of-band.
INV=$(curl -s -X POST https://auth.yieldfabric.com/auth/invitations \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"invitee_email": "them@example.com",
"message": "Join me on YieldFabric",
"target_kind": "connection"
}')
INVITE_URL=$(echo "$INV" | jq -r .invite_url) # {app}/invite/{token}
TOKEN_VALUE=$(echo "$INV" | jq -r .invitation.token)
# 2. The recipient previews the invitation BEFORE signing up — public, no auth.
curl -s "https://auth.yieldfabric.com/auth/invitations/$TOKEN_VALUE"
# 3. The recipient opens INVITE_URL, onboards (becomes a user), and obtains a
# JWT ($INVITEE_TOKEN). Their account email must match the invited address.
# 4. Accept — returns the typed next action to route the new user into.
curl -s -X POST "https://auth.yieldfabric.com/auth/invitations/$TOKEN_VALUE/accept" \
-H "Authorization: Bearer $INVITEE_TOKEN"
# => { "invitation": {…}, "next_action": { "kind": "connection", "payload": {…} } }
Things to know:
GET /auth/invitations/{token}is public — it returns a redacted view (inviter name, message, a curated target preview) and marks the invitationopened. Never send a bearer token to it.- Accept requires the caller's email to match
invitee_email(the link is bound to that inbox) — otherwise403. Onboard the invitee with the invited address; the app prefills + locks it in the onboarding wizard. - Follow
next_actionafter accept:obligation→ the accept-obligation surface,deal_flow→ the deal,group_join→ the group,none/connection→ home / contacts.connectionalso forms the inviter↔invitee connection server-side. - Lifecycle is
pending → opened → accepted(terminalrevoked/expired). Resend is rate-limited;DELETE /auth/invitations/{token}is an inviter-only soft revoke.
8. Handle errors
{ "error": "<string>" }. Route on HTTP status.errors[].extensions.code in envelope. Switch on code, not message.Status codes (consistent across services):
200/201/204— success400— validation failed, idempotency conflict, business-rule violation401— missing / expired / wrong-audience JWT (re-refresh)403— JWT valid but role/scope lacks permission404— resource doesn't exist (or visibility-scoped to a group you're not in)429— rate-limited (login failures only; the response is delayed but still returns 401)5xx— server-side; retry with same idempotencyKey
GraphQL error codes you'll see most:
INSUFFICIENT_BALANCE, IDEMPOTENCY_CONFLICT,
CONTRACT_STATE_INVALID, SWAP_STATE_INVALID,
DEAL_STATE_INVALID, COUNTERPART_NOT_FOUND, FORBIDDEN.
9. Check a deployment's API version before relying on a feature
Each service exposes an unauthenticated GET /version probe (a
sibling of /health). Use it to confirm the deployment you're talking
to actually serves the API version that documents the feature you're
about to call — useful at integration time, in a readiness gate, or
when a feature appears to be "missing" at runtime.
curl https://auth.yieldfabric.com/version
curl "$PAYMENTS_BASE/version" # selected payments REST host
curl https://agents.yieldfabric.com/version
# → {"service":"yieldfabric-agents","api_version":"0.12.1","git_sha":"…","built_at":"…"}
/version is a per-service REST route — hit each service on its own
host (the same hosts you use for auth REST, payments status polling,
and agents REST), not the GraphQL gateway.
The response is {service, api_version, git_sha, built_at}.
api_version is the service's OpenAPI info.version, baked into the
binary at compile time from its own spec — so a deployment can never
report a version it doesn't implement. If api_version is older than
the version that introduced a feature you depend on, you're pointed at
a stale deployment; that's the mismatch /version exists to surface
(previously you'd have had to probe individual routes for presence).
No Authorization header is needed — you can check this before
holding any credential. Full reference:
docs/versioning.md §"Discovering the served
version".
10. Read your LLM usage and cost (agents)
If your app uses the agents LLM surface (native chat, the
OpenAI-compatible /v1 endpoint, RAG, workflows), the agents service
meters every call and exposes it back to you, entity-scoped — you see
your own usage; staff may pass an entity_id for any entity.
# Live cross-surface rollup, grouped by (feature, model), over a window.
# Up-to-the-second; counts failed calls; spans every surface you used.
curl "https://agents.yieldfabric.com/api/usage/aggregate?start_date=2026-06-01&end_date=2026-06-16" \
-H "Authorization: Bearer $TOKEN"
# → { "data": [ { "feature": "chat", "model": "…", "calls": N,
# "prompt_tokens": N, "completion_tokens": N,
# "total_tokens": N, "error_count": N, "avg_latency_ms": N }, … ],
# "totals": { "calls": N, "prompt_tokens": N, "completion_tokens": N,
# "total_tokens": N, "error_count": N } }
# Per-call audit: the exact prompt messages + output for one metered call.
# The usage_event_id is the `id` on a GET /api/usage/detail row.
curl "https://agents.yieldfabric.com/api/usage/calls/$USAGE_EVENT_ID/log" \
-H "Authorization: Bearer $TOKEN"
Things to know:
- Streaming chat calls are audited too. A streamed completion
(every chat answer) records both its token usage and a call-log
transcript, so it shows up in
/api/usage/calls/{id}/log— no more "no log for this call" on streamed responses. - Embeddings are logless by design. Embedding calls (feature
embed) are metered — they appear in/api/usage/aggregatewith their token counts — but carry no call-log transcript, so don't expect/api/usage/calls/{id}/logto return one for them. avg_latency_msanderror_countcome from the raw usage table, so the aggregate includes failures and is up-to-the-second (unlike the day-bucketed/api/usage/summary).- Both endpoints require a bearer JWT and are entity-scoped; a
non-staff caller asking for another entity's
entity_idgets403, and opening another entity's call log gets403(or404if call logging is disabled / the row aged out of retention).
Service URLs at a glance
https://auth.yieldfabric.com/auth/**https://api.yieldfabric.com/graphqlhttps://pay.test.yieldfabric.com/graphqlhttps://pay.live.yieldfabric.com/graphqlhttps://agents.yieldfabric.com/**<selected-payments-host>/api/users/{user_id}/messages*Where to find detailed reference
/docs/api/auth/docs/api/payments/docs/api/agentsThe OpenAPI specs (3 of them) are the source of truth. Browse
them in the API explorer at /docs/api/<service>, or download the
bundled JSON for each service to generate SDKs from.
Type-safe clients
Generate TypeScript types from the served specs:
# Once
npm i -g openapi-typescript
# Download the bundled specs
curl -o /tmp/yf-auth.json https://yieldfabric.com/api/openapi/auth.json
curl -o /tmp/yf-payments.json https://yieldfabric.com/api/openapi/payments.json
curl -o /tmp/yf-agents.json https://yieldfabric.com/api/openapi/agents.json
# Generate types
npx openapi-typescript /tmp/yf-auth.json -o src/types/yf-auth.ts
npx openapi-typescript /tmp/yf-payments.json -o src/types/yf-payments.ts
npx openapi-typescript /tmp/yf-agents.json -o src/types/yf-agents.ts
Then use openapi-fetch for typed fetch calls:
import createClient from 'openapi-fetch'
import type { paths } from './types/yf-auth'
const auth = createClient<paths>({
baseUrl: 'https://auth.yieldfabric.com',
})
const { data, error } = await auth.POST('/auth/login', {
body: { email, password },
})
Other languages: see sdks.md.
Common pitfalls (and how to avoid them)
errors[].extensions.code inside the body.message textcode for routing; message may change between releases. Same for error strings in REST — those are human-readable.idempotencyKey with different bodiesIDEMPOTENCY_CONFLICT.idempotencyKey (e.g. sha256(user_id, action)) from app codemessageId without re-enqueueing — the executor never gets a fresh message to run. Either omit idempotencyKey (the resolver auto-generates one per call from timestamp+user) or use a fresh UUIDv4.acceptObligation immediately after createObligation returns success"Contract not found: CONTRACT-OBLIGATION-...". Poll GET /api/users/{user_id}/messages/{message_id} every 2s until executed is set first (or run a workflow that waits for it).acceptObligation after a self-counterpart payment-less createObligation (caller is also the counterparty AND no initialPayments)safeMint inlines the acceptance for this shape — both ObligationCreated and ObligationAccepted fire in the same receipt, and the contract row lands at COMPLETED in one step. A follow-up acceptObligation call is now an idempotent no-op success — the resolver detects the COMPLETED status and returns a synthesized success response without enqueueing a duplicate submission. Callers can keep their existing two-call pattern; the resolver handles the idempotency.403 Insufficient permissions: CryptoOperationsuser_permissions (this happens to anyone created via POST /auth/users on YF versions before the auto-grant landed). The JWT-mint path now falls back to role defaults when the table is empty, so users with SuperAdmin/Admin/Manager work out-of-the-box. For narrower roles, grant explicitly via POST /auth/users/{id}/permissions.0x… address as counterpart / obligor on createObligation (or as destinationId on instant)entitiesByName), not address matches. The resolver returns "No entity found with name '0x…'". Use the matching *WalletId field (counterpartWalletId, obligorWalletId, destinationWalletId) — pass the wallet UUID, e.g. from /protected/jwt's default_wallet_id./graphql selected by the active JWT chain. Keep federated/shared-core reads and auth/agents mutations on api.yieldfabric.com/graphql./api/events/stream) or WebSocket (/ws/messages) to wait for message completion from a backend service/api/users/{user_id}/messages/{message_id} — simpler, no missed-event race, no streaming state to manage. Match the Python SDK's poll_message_completion: 2s interval, 300s ceiling, terminal when executed is set.<payments-host>/api/mq/submit to mint obligations, send payments, etc.403 Insufficient permissions: CryptoOperations otherwise). Use payments-direct GraphQL mutations (createObligation, acceptObligation, instant, …) — they call into the same pipeline with proper input normalisation and entity resolution.Completed status, balance state may take a few seconds to converge in the consumer pipeline. Re-query payments after a 2-3s delay.acting_as), not a header. Mint a delegation JWT via POST /auth/delegation/jwt.payments-service audience) are for service-to-service calls only. Client apps use user tokens."Help, I want to build X"
/docs/api/auth/guides/getting-started.mdagents-and-workspaces.md — threads + streaming sectionsdms.md and the obligation operations in /docs/api/paymentsloans.md — the app-built loan deal (compose → sign → activate → live periodic repayment), sibling of the one-shot rental deal@yieldfabric/wallet SDK (drop-in React components)createObligation with counterpart set to the same user and no initialPayments. The on-chain safeMint auto-accepts this shape in a single call — ObligationCreated and ObligationAccepted fire in the same receipt and the contract lands at COMPLETED immediately. No follow-up acceptObligation needed.dms.md's doc_escrow template + . A payer escrows funds; a neutral attestor commits a zero-knowledge document; the beneficiary releases via a doc-gated data policy — composing commit_oracle_document, add_data_policy, and execute_under_policy.In one sentence
Get a JWT from auth.yieldfabric.com/auth/login; use api.yieldfabric.com/graphql
for federated reads, the JWT-selected payments /graphql for chain-bound
mutations, that same payments host's /api/users/.../messages to poll
settlement, and agents.yieldfabric.com for agents REST/SSE. Omit
idempotencyKey or use a fresh UUIDv4 per logical mutation. Switch on
errors[].extensions.code (GraphQL) or HTTP status (REST). Treat
refresh tokens as single-use.
That's the platform.