Reference
The tables you come back for. Every error code here is read from @limen/chain, and every hash from the deployments file — none of it is transcribed into this page.
Contract error codes
Decoded from a failed transaction’s own diagnostic events. The boundary column is the distinction the whole product turns on: a boundary refusal is evidence the rule did its job, whereas a missing rule or a malformed call is not. ContextRuleNotFound is deliberately excluded — “the boundary refused you” and “the boundary is gone” are different claims.
| Code | Name | Boundary |
|---|---|---|
| 3000 | ContextRuleNotFound smart account | no rule |
| 3002 | UnvalidatedContext smart account | refusal |
| 3003 | ExternalVerificationFailed smart account | — |
| 3004 | NoSignersAndPolicies smart account | — |
| 3005 | PastValidUntil smart account | — |
| 3006 | SignerNotFound smart account | — |
| 3007 | DuplicateSigner smart account | — |
| 3008 | PolicyNotFound smart account | — |
| 3009 | DuplicatePolicy smart account | — |
| 3010 | TooManySigners smart account | — |
| 3011 | TooManyPolicies smart account | — |
| 3012 | MathOverflow smart account | — |
| 3013 | KeyDataTooLarge smart account | — |
| 3014 | ContextRuleIdsLengthMismatch smart account | refusal |
| 3015 | NameTooLong smart account | — |
| 3016 | UnauthorizedSigner smart account | refusal |
| 3220 | SmartAccountNotInstalled spending limit | — |
| 3221 | SpendingLimitExceeded spending limit | refusal |
| 3222 | InvalidLimitOrPeriod spending limit | — |
| 3223 | NotAllowed spending limit | refusal |
| 3224 | HistoryCapacityExceeded spending limit | — |
| 3225 | AlreadyInstalled spending limit | — |
| 3226 | LessThanZero spending limit | — |
| 3227 | OnlyCallContractAllowed spending limit | — |
Policy primitives
The closed set of things a derived boundary can be lowered into. Both are configurations of existing audited code — adding a third means deploying a third audited primitive, not generating one.
| Kind | Configures | Refuses |
|---|---|---|
| spending_limit OpenZeppelin spending-limit policy | asset, limit, windowLedgers | an outflow above the cap within the window |
| function_allowlist context rule scoping | contractId, functions | any function on that contract outside the list |
The six deny axes
Each axis changes exactly one dimension of the permitted flow. These are the attempts made against a real rule on a real account; every one reached a ledger and failed there.
| Verdict | Axis | Attempt | Simulated / on ledger | Hash |
|---|---|---|---|---|
| DENY | amount | transfer above the cap | SpendingLimitExceeded#3221 SpendingLimitExceeded#3221 | ac477549…7a77 |
| DENY | function | approve on the permitted token contract | NotAllowed#3223 NotAllowed#3223 | 45a0eb20…1f58 |
| DENY | asset | transfer of a different token | UnvalidatedContext#3002 UnvalidatedContext#3002 | 1312be89…4880 |
| DENY | contract | execute on the smart account itself | UnvalidatedContext#3002 UnvalidatedContext#3002 | 6b7f4ded…9389 |
| DENY | invocation | appended second invocation | ContextRuleIdsLengthMismatch#3014 ContextRuleIdsLengthMismatch#3014 | e365e681…7149 |
| DENY | expiry | same call after valid_until | UnvalidatedContext#3002 trapped; contract code not recovered from diagnostics | f5ebce51…8405 |
Both columns are shown because they are different claims. The simulation error is what the RPC predicted; the ledger error is what the network actually returned, decoded from diagnostic events. They agree on five of six — the sixth reached a ledger and failed there, but its contract code was not recovered from that run’s diagnostics, and the recording says so rather than filling it in.
Environment variables
None of these is required to read the site or the documentation, and most have a fallback or a route that declines cleanly in their absence — a missing variable never degrades into a screen that silently shows something wrong. Two are exceptions, and both refuse rather than degrade on the production deployment: the shared store, because the fallback there would be a rate limit counted per instance rather than in total; and the passkey relying party, because the fallback would be trusting the caller’s own headers to say which site an assertion came from. Elsewhere both fall back and say so.
| Variable | Scope | Purpose |
|---|---|---|
| NEXT_PUBLIC_STELLAR_RPC_URL | browser | Soroban RPC endpoint for reads. Falls back to the public testnet endpoint. |
| SOROBAN_RPC_URL | server | Server-side RPC endpoint, used by the routes that keep the SDK out of the browser bundle. |
| NEXT_PUBLIC_SMART_ACCOUNT_ID | browser | The account the app screens open on when no other is chosen. |
| NEXT_PUBLIC_SITE_URL | browser | Absolute origin for share-card URLs. Vercel supplies one automatically in production. |
| VERCEL_PROJECT_PRODUCTION_URL | platform | Supplied by Vercel, not set by hand. Used as the share-card origin when NEXT_PUBLIC_SITE_URL is absent. |
| ANTHROPIC_API_KEY | server | Enables the explain endpoint. Absent, that route declines rather than degrading silently. |
| LIMEN_DEMO_SECRET | server | A disposable testnet key used only by the scripted demo transfer. Never a user key. |
| LIMEN_DEMO_DESTINATION | server | Where the scripted demo transfer sends its testnet dust. |
| LIMEN_SIMULATION_SOURCE | server | Source account used for read-only simulation, which costs no fee and needs no signature. |
| WAITLIST_STORE_PATH | server | Where waitlist entries are written. Defaults to a file in the system temp directory. |
| LIMEN_ERROR_WEBHOOK | server | Where error reports are delivered. Server-side only, because a webhook URL is a credential. Set for preview deployments as well as production; preview reports are prefixed with their environment so they do not read as production incidents. Unset, a report is logged and not sent. |
| VERCEL_ENV | server | Which deployment this is — production, preview or development. Read for two things: labelling an error report, so a preview experiment does not read as a production incident in the same channel; and deciding whether a missing shared store is a refusal or a fallback, since Vercel sets NODE_ENV=production for previews too and it therefore cannot answer that question. Set by the platform, not by hand; absent, the label is omitted rather than guessed. |
| UPSTASH_REDIS_REST_URL | server | The shared store behind the rate limits and the transaction cache, over HTTP because a serverless function has no connection to pool. This is the one entry in this table the production deployment will not start without: falling back to per-instance counters there would enforce a limit per instance rather than in total, which is the behaviour V8 M1 set out to retire. On a preview or in development the fallback is allowed and is logged to stderr. |
| UPSTASH_REDIS_REST_TOKEN | server | Credential for the above. Both are needed together — either one alone counts as unset, because half a configuration is not a store. |
| DATABASE_URL | server | Postgres, holding users and sessions as of V8 M1. Reached over neon-http from the web app, which sends each query as an HTTP request and therefore has no connection to exhaust across many function instances — and, as the cost of that, cannot run interactive transactions. There is no fallback: a route that needs the database refuses and names this variable, because a session that does not survive the next request is not a session. Migrations use the direct, unpooled endpoint rather than this one. |
| LIMEN_RUNTIME_URL | server | Where apps/runtime is reachable. The web chat accepts a message, asks the model which tool it wants, and hands that tool to the runtime — which owns execution because a turn takes 15–45 seconds and can move money, and a payment in flight inside a request handler is a payment that disappears when the handler does. There is deliberately no localhost fallback: a default would make a misconfigured deployment fail by quietly trying to reach a runtime that is not there, surfacing as a timeout on the money path rather than as the configuration error it is. Unset is reported as unset, and the chat says so instead of reporting a failed turn. |
| LIMEN_WEBAUTHN_RP_ID | server | The relying party a passkey is bound to — a registrable domain such as limen.app, never an origin. Required on the production deployment, which refuses to start without it: the alternative is deriving the expected origin from the request, and the Origin and Host headers are supplied by the caller, so that check would accept a replayed assertion from any site while looking exactly like a working login. Outside production it defaults to localhost so the ceremony still runs. |
| LIMEN_WEBAUTHN_ORIGINS | server | Comma-separated list of origins an assertion may come from, matched exactly and never by prefix. Checked because the on-chain verifier validates neither origin nor rpIdHash, so a valid assertion proves which credential signed but not which site asked. |
| VERCEL_URL | platform | This deployment's own hostname, set by the platform and not by hand. Added to the accepted passkey origins so a preview can run the login ceremony without its URL being configured per branch. Safe in a way a request header is not, because it comes from the deployment's environment rather than the caller — though a passkey registered on production still will not work on a preview, since the two are different relying parties. |
| NODE_ENV | platform | Set by the toolchain, never by hand. Read only as the fallback answer to "is this production" where the platform does not say — a self-hosted container, which has no preview concept. On Vercel it is production for preview builds too, which is why VERCEL_ENV is preferred over it. |
| NEXT_PUBLIC_LIMEN_RELEASE | browser | The short commit SHA an error report names as its build. Derived from the platform at build time, not set by hand. |
No secret key appears in any of these except LIMEN_DEMO_SECRET, which is a disposable testnet key used only by the scripted demo transfer. No user key is ever read from the environment, because no user key ever leaves the browser that generated it.