+
Install
@@ -318,13 +325,13 @@ export default function Page() { key={system} className="block border-b border-rule py-3 sm:table-row sm:border-b-0 sm:py-0" > -
Keys
++ Keys +
In the shell,
+ Create a key with only Query Funds and Query Open Orders & Trades turned on.
+
+ Kraken reports the key’s whole permission list, from an endpoint that needs no
+ permission of its own. A key that can trade or withdraw is refused.
+
+ Spot, staked and held balances in every wallet, and open spot-margin positions.
+
+ The account margin level, Kraken Futures positions and drawn credit lines. See{' '}
+ why that matters for ranking.
+
+ Install tula, then connect with the key:
+
+ The same on Binance and{' '}
+ Coinbase. How keys are kept.
+
+
+ Aave: from the health factor.{' '}
+ More on Aave.
+
+ Hyperliquid: from the liquidation
+ price, or the account ratio under unified or portfolio margin.{' '}
+ More on Hyperliquid.
+
+ Coinbase perps: from the liquidation
+ price Coinbase publishes. More on Coinbase.
+
+ A position with nothing to rank on sorts last, as unknown, never as safe.
+
+ When a venue has an area tula does not read that could hold a liquidation,{' '}
+
+ Install tula, connect your venues, then:
+ 404
- The link may have moved, or the address may be mistyped. Everything the site publishes is
- below.
+ The link may have moved, or the address may be mistyped. The main pages are below.
Your true exposure, what breaks first, and more, across every venue at once.
diff --git a/site/app/security/page.tsx b/site/app/security/page.tsx
index 9f93a42..b215b04 100644
--- a/site/app/security/page.tsx
+++ b/site/app/security/page.tsx
@@ -6,13 +6,19 @@ import { NAME, OG, REPO, TWITTER } from '@/lib/site'
const TITLE = 'Security model — non-custodial, and read-only'
const SUMMARY =
- 'What tula promises about your keys and your funds, and what enforces each promise: no code path can move funds off a venue, a key that can withdraw is turned away, and your keys never reach the model.'
+ 'What tula promises about your keys and your funds, and what enforces each promise. No code path can move funds off a venue, and your keys never reach the model.'
export const metadata: Metadata = {
title: TITLE,
description: SUMMARY,
alternates: { canonical: '/security' },
- openGraph: { ...OG, type: 'website', url: '/security', title: TITLE, description: SUMMARY },
+ openGraph: {
+ ...OG,
+ type: 'website',
+ url: '/security',
+ title: `${TITLE} · ${NAME}`,
+ description: SUMMARY,
+ },
twitter: { ...TWITTER, title: `${TITLE} · ${NAME}`, description: SUMMARY },
}
@@ -46,7 +52,7 @@ const NOTES = [
],
[
'Unknown is a value',
- 'Kraken proves a key cannot withdraw, not that it cannot trade, so tula says unknown rather than safe. A missing price shows as missing, never as zero.',
+ 'Stripe publishes no way to check what a key can do, so tula says unknown rather than safe. A missing price shows as missing, never as zero.',
],
[
'The model never computes',
@@ -62,7 +68,7 @@ const NOTES = [
],
[
'What tula connects to',
- 'The venues you connect, your price source, token lists, and public nodes for Ethereum, Arbitrum One, Base, Polygon, Optimism, Avalanche, Gnosis, Scroll and Linea. If a node is down tula tries the next one, which then also sees your address. Anthropic gets only finished numbers, and only when you ask a question. The shell checks GitHub for a new release once a day. The binary tracks nothing; this site uses Google Analytics.',
+ 'The venues you connect, your price source, token lists, and public nodes for Ethereum, Arbitrum One, Base, Polygon, Optimism, Avalanche, Gnosis, Scroll and Linea. If a node is down tula tries the next one, which then also sees your address. Anthropic gets only finished numbers, and only when you ask a question. The shell checks GitHub for a new release each time it opens. The binary tracks nothing; this site uses Google Analytics and shows a Peerlist badge.',
],
] as const
@@ -70,7 +76,7 @@ export default function Page() {
return (
? on an empty line shows these, and /keys prints
them.
diff --git a/site/app/kraken/page.tsx b/site/app/kraken/page.tsx
new file mode 100644
index 0000000..124ff2b
--- /dev/null
+++ b/site/app/kraken/page.tsx
@@ -0,0 +1,60 @@
+import { Guide, guideMetadata, Section } from '@/components/Guide'
+import { Link } from '@/components/Link'
+import { Terminal } from '@/components/Terminal'
+
+const PATH = '/kraken'
+const TITLE = 'Kraken read-only API key for portfolio tracking'
+const SUMMARY =
+ 'Which Kraken API key permissions to turn on so a tool can read balances but never withdraw, and what tula refuses.'
+
+export const metadata = guideMetadata(PATH, TITLE, SUMMARY)
+
+export default function Page() {
+ return (
+ breaks lists every position that can be liquidated, nearest first, with the
+ price move that liquidates it.
+ breaks says so under the list. Kraken’s account margin level is one.{' '}
+ More on Kraken.
+
+
There is nothing at this address.
Every page
+ Pages
- {NAV.map(({ href, label, blurb }) => (
+ {NAV.filter((n) => n.group === 'site').map(({ href, label, blurb }) => (
Every venue weighs only what it holds.
-
- tula weighs what you hold.
+ {' '}
+ tula weighs what you hold.
+
Security model
{/* Two paragraphs: run together, the second sentence starts mid-line. */}
diff --git a/site/app/sitemap.ts b/site/app/sitemap.ts
index 3bd3a53..fa53f6e 100644
--- a/site/app/sitemap.ts
+++ b/site/app/sitemap.ts
@@ -8,24 +8,11 @@ export const dynamic = 'force-static'
* Every readable page is `NAV` plus `llms.txt`, derived rather than listed so a
* new page cannot ship unindexed. What is left out is what a reader would
* never arrive at: the 404, the assets, `install.sh` and `security.txt`.
+ *
+ * URLs only. Google ignores `priority` and `changefreq`, and uses `lastmod`
+ * only where it is accurate — the build time on every URL is not, and the
+ * deploy's shallow clone cannot give each page its own date.
*/
export default function sitemap(): MetadataRoute.Sitemap {
- // The site is static, so "last modified" is the build that published it —
- // which is the only change a crawler could ever be told about.
- const built = new Date()
-
- return [
- ...NAV.map(({ href }) => ({
- url: pageUrl(href),
- lastModified: built,
- changeFrequency: 'monthly' as const,
- priority: href === '/' ? 1 : 0.8,
- })),
- {
- url: `${SITE}/llms.txt`,
- lastModified: built,
- changeFrequency: 'monthly' as const,
- priority: 0.5,
- },
- ]
+ return [...NAV.map(({ href }) => ({ url: pageUrl(href) })), { url: `${SITE}/llms.txt` }]
}
diff --git a/site/components/Channels.tsx b/site/components/Channels.tsx
index 20719a6..b334c54 100644
--- a/site/components/Channels.tsx
+++ b/site/components/Channels.tsx
@@ -13,9 +13,9 @@ export interface Channel {
const slug = (name: string) => name.toLowerCase().replace(/[^a-z0-9]+/g, '-')
/**
- * One install channel at a time. All three used to run down the page at once,
- * and every section under them branched three ways in prose — a reader on any
- * one channel read two answers that were not theirs to find the one that was.
+ * One install channel at a time. With all three down the page, every section
+ * under them branches three ways, and a reader on one channel reads two answers
+ * that are not theirs to find the one that is.
*
* Panels hide rather than unmount, so the static export ships all three in the
* HTML and a crawler indexes the whole page, not whichever tab happened to be
diff --git a/site/components/Copy.tsx b/site/components/Copy.tsx
index 3ca8b2b..3f8a333 100644
--- a/site/components/Copy.tsx
+++ b/site/components/Copy.tsx
@@ -20,10 +20,10 @@ const WORDS: Record