Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
ac55438
feat(dapp): connect a wallet on every chain, behind a wallet gate
0xDeon Nov 4, 2026
9bd7280
feat(dapp): swap, lend and earn on solana
0xDeon Nov 4, 2026
da5a9c2
feat(dapp): swap, supply and stake on sui
0xDeon Nov 4, 2026
326753f
feat(dapp): swap, earn and stake on near
0xDeon Nov 4, 2026
d6e7d13
feat(dapp): cash out usdc through weave on five chains
0xDeon Nov 4, 2026
10b57f8
fix(dapp): accept weave's real order amounts on a live test
0xDeon Nov 4, 2026
4badef4
docs: plan the agent race on every chain
0xDeon Nov 4, 2026
371f974
feat(dapp): let the agent race take a chain action
0xDeon Nov 4, 2026
d0bd890
fix(dapp): refuse a bad race amount before quoting
0xDeon Nov 4, 2026
e98d16e
feat(dapp): give agents each chain's own facts
0xDeon Nov 4, 2026
1f5c6ca
fix(dapp): keep an unpriced swap from winning and show dollars on cha…
0xDeon Nov 4, 2026
eee715a
feat(dapp): quote near venues for the agent race
0xDeon Nov 4, 2026
47fbf15
feat(dapp): run the agent race on near from chat
0xDeon Nov 4, 2026
bac07c1
fix(dapp): keep chain routes off the swap hook and close the race's s…
0xDeon Nov 4, 2026
80e0dac
fix(dapp): hold the signed latch for the whole race and check the wal…
0xDeon Nov 4, 2026
225af35
fix(dapp): report an unknown submit outcome as pending so the latch h…
0xDeon Nov 4, 2026
26019ea
fix(dapp): only call a solana submit clean when the answer proves not…
0xDeon Nov 4, 2026
1a5fe73
feat(dapp): race jupiter and cetus against 1click
0xDeon Nov 4, 2026
f67809a
fix(dapp): value 1click like every other route and answer inside the …
0xDeon Nov 4, 2026
2bf47ab
feat(dapp): swap and race on avalanche and monad through 1click
0xDeon Nov 4, 2026
ededd65
fix(dapp): bind an evm revert to this transfer and read the chain's n…
0xDeon Nov 4, 2026
1901c7b
fix(dapp): answer pending when an evm revert or amount can't be read
0xDeon Nov 4, 2026
b12fe60
feat(dapp): record the chain of every agent race
0xDeon Nov 4, 2026
b77e577
fix(dapp): read no stellar network when loading the chain name list
0xDeon Nov 4, 2026
c80ef06
fix(dapp): send a chain submit's own race id and match evm accounts b…
0xDeon Nov 4, 2026
d396497
fix(dapp): hold the card's venue on a second press and name the venue…
0xDeon Nov 4, 2026
e2d074c
fix(dapp): say which chain each race in an account's history ran on
0xDeon Nov 4, 2026
5a52aa0
fix(dapp): read an amount like .5 as 0.5 so its build isn't refused
0xDeon Nov 4, 2026
9ac07f9
fix(dapp): refuse a race with no venue and stop promising what a chai…
0xDeon Nov 4, 2026
c851aa7
fix(dapp): warn about a chain transfer already started, even after a …
0xDeon Nov 4, 2026
1e8e136
docs: plan multi-step transactions on every chain
0xDeon Nov 4, 2026
faecfd9
feat(dapp): show each payout currency's country flag
0xDeon Nov 4, 2026
0820643
feat(dapp): read every token a wallet holds on each chain
0xDeon Nov 4, 2026
1545f84
fix(dapp): read held tokens from the chain, bound the read and drop s…
0xDeon Nov 4, 2026
4f50614
fix(dapp): never leave the chat busy after a dropped balance reply, a…
0xDeon Nov 4, 2026
725033c
fix(dapp): show a disconnected person's balance reply instead of a wa…
0xDeon Nov 4, 2026
ff736b8
feat(dapp): keep the chat until a new chat starts
0xDeon Nov 4, 2026
15d23c1
fix(dapp): start a switched wallet fresh and keep private figures out…
0xDeon Nov 4, 2026
53ba04b
fix(dapp): make the wallet switch notice true after a cash-out is paid
0xDeon Nov 4, 2026
6d3a4fe
feat(dapp): read a swap and what comes after it
0xDeon Nov 4, 2026
a820022
fix(dapp): keep a refused currency, say what reached the wallet, and …
0xDeon Nov 4, 2026
0128bc6
feat(dapp): swap then cash out on solana, sui and avalanche
0xDeon Nov 4, 2026
a67ea86
fix(dapp): check a chain sequence's words, balance and size before th…
0xDeon Nov 4, 2026
501f601
fix(dapp): read a chain's name as its token in what a person types
0xDeon Nov 4, 2026
43b9f55
feat(dapp): swap then deposit, stake or send on every chain
0xDeon Nov 4, 2026
16327e7
fix(dapp): close the race once a sequence's swap settles, and check w…
0xDeon Nov 4, 2026
3f73acc
fix(dapp): read no thousands from a leading zero, and no swap from a …
0xDeon Nov 4, 2026
8a797d2
feat(dapp): read multi-step requests in ordinary words on every chain
0xDeon Nov 4, 2026
f7cee9a
feat(dapp): hold a chain's readings to its own tokens, and gate the t…
0xDeon Nov 4, 2026
68ffc8e
fix(dapp): keep a swap's order in the person's words, and stop tellin…
0xDeon Nov 5, 2026
0d42660
fix(dapp): keep step 2 from offering the swap again, and stop near ca…
0xDeon Nov 5, 2026
d375023
fix(dapp): report a follow-on the server can't honour, off stellar only
0xDeon Nov 5, 2026
2e7f0dc
fix(dapp): send ordinary words to the model, and hold step 2 to what …
0xDeon Nov 5, 2026
3134419
fix(dapp): pass no figure with a decimal comma, and say why a message…
0xDeon Nov 5, 2026
4d071dc
fix(dapp): size dollar amounts at a live price, and stop reading $5 o…
0xDeon Nov 5, 2026
42fb856
fix(dapp): let no model choose between dollars and a count, and price…
0xDeon Nov 5, 2026
7433891
fix(dapp): say which agents didn't answer, and tell them a later step…
0xDeon Nov 5, 2026
3d039b6
docs(dapp): plan limit orders off stellar, with the venue research
0xDeon Nov 5, 2026
3c77f0e
docs(dapp): always state a recommended strategy off stellar
0xDeon Nov 5, 2026
c9e7848
feat(dapp): read a limit order and do its price sums
0xDeon Nov 5, 2026
8e4e9c8
fix(dapp): make a limit order's side and token received the person's …
0xDeon Nov 5, 2026
7fa2f89
feat(dapp): route limit orders and refuse them where no venue is safe
0xDeon Nov 5, 2026
11197c2
fix(dapp): check an exit's signing and count a limit's setup cost wha…
0xDeon Nov 5, 2026
a97fc8d
feat(dapp): place, read and close kamino limit orders on solana
0xDeon Nov 5, 2026
ff96a07
fix(dapp): verify a limit order's own signature and say its fee comes…
0xDeon Nov 5, 2026
c48feec
feat(dapp): place, read and collect deepbook limit orders on sui
0xDeon Nov 5, 2026
8ba349d
fix(dapp): refuse a merge into a deepbook deposit and correct its notes
0xDeon Nov 5, 2026
9730ea7
feat(dapp): place, read and collect rhea limit orders on near
0xDeon Nov 5, 2026
220fd53
fix(dapp): refuse a limit order the wallet can't fund, and note rhea'…
0xDeon Nov 5, 2026
cfdb78f
feat(dapp): sign and place cow limit orders on avalanche
0xDeon Nov 5, 2026
a2f50a5
test(dapp): read and quote cow on avalanche mainnet
0xDeon Nov 5, 2026
dc990fe
test(dapp): let an avalanche or monad venue claim execution
0xDeon Nov 5, 2026
a2cd614
fix(dapp): refuse a second open cow sale of a token, and read a dupli…
0xDeon Nov 5, 2026
e6be6fb
feat(dapp): let agents recommend resting an order where a venue can h…
0xDeon Nov 5, 2026
afc0923
feat(dapp): place limit orders from the chat, and list, cancel and co…
0xDeon Nov 5, 2026
d6dd14d
feat(dapp): let the model read a limit order, held to the persons own…
0xDeon Nov 5, 2026
429c7fa
docs(dapp): format the limit order research
0xDeon Nov 5, 2026
83f6648
fix(dapp): hold an implied sale to selling words, refuse contract wal…
0xDeon Nov 5, 2026
fb4efc5
fix(dapp): refuse an implied sale when the person used a buying word
0xDeon Nov 5, 2026
bbf9d85
docs(dapp): format the sui limit order research
0xDeon Nov 5, 2026
3ab12ac
chore(dapp): merge limit orders onto the landed stack (#156), resolvi…
0xDeon Nov 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 31 additions & 3 deletions apps/dapp/app/api/agents/compete/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import {
assertKnownTokens,
assertNativeReserve,
} from '../../../../lib/chains/prepare'
import { limitPair } from '../../../../lib/chains/limit'
import { restOption, restRoute } from '../../../../lib/agents/rest-option'
import { chainModule } from '../../../../lib/chains/registry'
import {
ChainActionRefused,
Expand Down Expand Up @@ -172,8 +174,18 @@ async function chainRaceFacts(
// The chain's market already leaves out Stellar's assets, prices, lending
// rates and anchors; see `buildMarketContext`.
const base = await buildMarketContextAsync(chain)
const pair = action.kind === 'swap' ? limitPair(action.tokenIn, action.tokenOut ?? '') : undefined
const baseToken = chainApi.tokens.find((t) => t.symbol.toUpperCase() === pair?.base)
const marketUsd =
pair === undefined || baseToken === undefined
? undefined
: pair.selling
? price
: await chainApi.priceUsd(baseToken).catch(() => undefined)
const rest = marketUsd === undefined ? undefined : restOption(chainApi, action, marketUsd)
const market: MarketContext = {
...base,
...(rest !== undefined ? { rest } : {}),
prices: price !== undefined && tokenIn !== undefined ? { [tokenIn.symbol]: price } : {},
venues: chainApi.venues
.filter((v) => routes.some((r) => r.id === `${v.id}-1`))
Expand Down Expand Up @@ -519,19 +531,31 @@ export async function POST(request: Request): Promise<Response> {
const settleAndMeasure = (proposal: AgentProposalResult): AgentProposalResult => {
const own = (market.routes ?? []).find((r) => r.id === proposal.routeId)
return measured(
chainFacts === undefined ? settlePlan(proposal, intent, book) : settleChainPlan(proposal),
chainFacts === undefined
? settlePlan(proposal, intent, book)
: settleChainPlan(proposal, own !== undefined ? market.rest : undefined),
own?.quote as SwapQuote | ChainRouteQuote | undefined,
market.prices
)
}

// A plan that rests an order signs the order, not the swap it was measured against.
const sendProposal = (proposal: AgentProposalResult): void => {
const own = (market.routes ?? []).find((r) => r.id === proposal.routeId)
send({
type: 'competition:proposal',
competitionId,
proposal,
...(own !== undefined ? { route: own.quote } : {}),
...(own !== undefined
? {
route:
proposal.executionMode === 'rest' &&
market.rest !== undefined &&
proposal.restPriceUsd !== undefined
? restRoute(own.quote as ChainRouteQuote, market.rest, proposal.restPriceUsd)
: own.quote,
}
: {}),
})
}

Expand Down Expand Up @@ -740,7 +764,11 @@ export async function POST(request: Request): Promise<Response> {
...(unmeasuredRoutes.length > 0 ? { unmeasuredRoutes } : {}),
}

const scored = scoreProposals(proposals, { competitionId, plan })
const scored = scoreProposals(proposals, {
competitionId,
plan,
...(chainFacts !== undefined ? { restsBelowFills: true } : {}),
})
// An agent that held off has no plan to score.
const heldOff = new Set(proposals.filter((p) => p.held === true).map((p) => p.agent))
const winner = pickWinner(scored)
Expand Down
97 changes: 97 additions & 0 deletions apps/dapp/app/api/chain/[chain]/orders/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { NextResponse } from 'next/server'
import { z } from 'zod'

import { chainModule } from '../../../../../lib/chains/registry'
import { BALANCE_DEADLINE_MS, ChainActionRefused } from '../../../../../lib/chains/types'
import { enforceRateLimit } from '../../../../../lib/server/rate-limit'
import { reportError } from '../../../../../lib/server/report'
import { isWalletAddress } from '../../../../../lib/wallet/address'

export const runtime = 'nodejs'
export const dynamic = 'force-dynamic'
export const maxDuration = 20

const Body = z.object({
account: z.string().min(1).max(128),
orderId: z.string().min(1).max(200),
action: z.enum(['cancel', 'collect']),
})

/** The account's resting limit orders on this chain, read when asked and never kept. */
export async function GET(
request: Request,
{ params }: { params: Promise<{ chain: string }> }
): Promise<NextResponse> {
const limited = await enforceRateLimit(request, 'read')
if (limited !== undefined) return limited

const chainApi = chainModule((await params).chain)
if (chainApi === undefined) return NextResponse.json({ error: 'unknown_chain' }, { status: 404 })

const account = new URL(request.url).searchParams.get('account') ?? ''
if (!isWalletAddress(account)) {
return NextResponse.json({ error: 'invalid_account' }, { status: 400 })
}
if (chainApi.limitBook === undefined) {
return NextResponse.json({ orders: [] }, { headers: { 'cache-control': 'no-store' } })
}
try {
const orders = await chainApi.limitBook.openOrders(
account,
AbortSignal.timeout(BALANCE_DEADLINE_MS)
)
return NextResponse.json({ orders }, { headers: { 'cache-control': 'no-store' } })
} catch (e) {
if (e instanceof ChainActionRefused) {
return NextResponse.json({ error: 'invalid_account' }, { status: 400 })
}
reportError('chain/orders', e, { chain: chainApi.chain })
return NextResponse.json({ error: 'orders_unavailable' }, { status: 503 })
}
}

/** Builds the cancel or the collect of a resting order for the wallet to sign. */
export async function POST(
request: Request,
{ params }: { params: Promise<{ chain: string }> }
): Promise<NextResponse> {
const limited = await enforceRateLimit(request, 'build')
if (limited !== undefined) return limited

const chainApi = chainModule((await params).chain)
if (chainApi === undefined) return NextResponse.json({ error: 'unknown_chain' }, { status: 404 })

const parsed = Body.safeParse(await request.json().catch(() => undefined))
if (!parsed.success) return NextResponse.json({ error: 'invalid_body' }, { status: 400 })
if (!isWalletAddress(parsed.data.account)) {
return NextResponse.json({ error: 'invalid_account' }, { status: 400 })
}

if (chainApi.limitBook === undefined) {
return NextResponse.json(
{ error: 'There are no limit orders to change on this chain. Nothing was signed or sent.' },
{ status: 422 }
)
}
try {
const { account, orderId, action } = parsed.data
const prepared = await chainApi.limitBook.prepareExit(account, orderId, action)
const venue = chainApi.venues.find((v) => v.id === prepared.venueId)
if (venue === undefined) {
throw new ChainActionRefused(
'That order is not one I can build safely. Nothing was signed or sent.'
)
}
venue.assertSafe(prepared.signing, account, prepared.context)
return NextResponse.json({ prepared })
} catch (e) {
if (e instanceof ChainActionRefused) {
return NextResponse.json({ error: e.message }, { status: 422 })
}
reportError('chain/orders', e, { chain: chainApi.chain })
return NextResponse.json(
{ error: 'This could not be built right now. Nothing was signed or sent.' },
{ status: 502 }
)
}
}
4 changes: 4 additions & 0 deletions apps/dapp/app/api/chain/[chain]/prepare/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const Body = z.object({
'supply',
'stake',
'send',
'limit',
'withdraw',
'unstake',
'borrow',
Expand All @@ -37,6 +38,7 @@ const Body = z.object({
tokenOut: z.string().min(1).max(16).optional(),
venue: z.string().min(1).max(40).optional(),
recipient: z.string().min(1).max(128).optional(),
limitPrice: z.string().min(1).max(40).optional(),
durationDays: z
.string()
.regex(/^[1-9]\d{0,4}$/)
Expand Down Expand Up @@ -81,6 +83,7 @@ export async function POST(
tokenOut,
venue,
recipient,
limitPrice,
durationDays,
dueDate,
invoiceId,
Expand All @@ -95,6 +98,7 @@ export async function POST(
...(tokenOut !== undefined ? { tokenOut } : {}),
...(venue !== undefined ? { venue } : {}),
...(recipient !== undefined ? { recipient } : {}),
...(limitPrice !== undefined ? { limitPrice } : {}),
...(durationDays !== undefined ? { durationDays } : {}),
...(dueDate !== undefined ? { dueDate } : {}),
...(invoiceId !== undefined ? { invoiceId } : {}),
Expand Down
11 changes: 7 additions & 4 deletions apps/dapp/app/api/chain/[chain]/submit/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const Signed = z.discriminatedUnion('family', [
}),
// The venue checks its shape, so an odd answer from a wallet that may have sent is read as pending.
z.object({ family: z.literal('evm'), hash: z.string().max(128) }),
z.object({ family: z.literal('evm-typed'), signature: z.string().min(1).max(200) }),
])

const Body = z.object({
Expand Down Expand Up @@ -59,14 +60,16 @@ export async function POST(

const venue = chainApi.venues.find((v) => v.id === venueId)
if (venue === undefined) return NextResponse.json({ error: 'unknown_venue' }, { status: 404 })
// By family, since Avalanche and Monad are both EVM.
if (signed.family !== CHAIN_DESCRIPTORS[chainApi.chain].family) {
// By family, since Avalanche and Monad are both EVM. A typed message is EVM's too.
const family = signed.family === 'evm-typed' ? 'evm' : signed.family
if (family !== CHAIN_DESCRIPTORS[chainApi.chain].family) {
return NextResponse.json({ error: 'wrong_chain' }, { status: 400 })
}

// NEAR and EVM wallets have already sent, so there are no bytes left to check. Their venues
// ask the chain what was sent instead.
if (signed.family !== 'near' && signed.family !== 'evm') {
// ask the chain what was sent instead. A typed message carries only its signature, so its venue
// rebuilds what was signed and checks it before posting anything.
if (signed.family === 'solana' || signed.family === 'sui') {
const payload: SigningPayload =
signed.family === 'solana'
? { family: 'solana', transaction: signed.signedTransaction }
Expand Down
27 changes: 26 additions & 1 deletion apps/dapp/components/intents/chain-action-confirm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { ExternalLink, Loader2 } from 'lucide-react'

import type { ChainAction } from '../../hooks/use-chain-action'
import { stepErrorMessage } from '../../lib/agents/chain-race'
import { limitSettledNote } from '../../lib/chains/orders-view'
import { useChain } from '../../providers/chain-provider'
import type { PreparedAction } from '../../lib/chains/types'

function cardLine({ kind, pay, venueName }: PreparedAction): string {
Expand Down Expand Up @@ -45,6 +47,7 @@ export function ChainActionConfirm({
typedStep?: string | undefined
}): JSX.Element {
const { phase, prepared, hash, explorerUrl, earlier } = action
const { slug } = useChain()
const error = stepErrorMessage(action.error, sequenceSettled, typedStep)
const busy = phase === 'signing' || phase === 'submitting'

Expand Down Expand Up @@ -137,7 +140,29 @@ export function ChainActionConfirm({
</p>
) : null}

{phase === 'settled' ? (
{phase === 'settled' && action.canContinue ? (
<div className="flex flex-col gap-2">
<p className="flex items-center gap-2 text-sm font-medium" role="status">
Done. One more signature finishes this.
</p>
<div>
<Button size="sm" onClick={action.continueSetup}>
Continue
</Button>
</div>
</div>
) : null}

{phase === 'settled' &&
!action.canContinue &&
prepared.kind === 'limit' &&
action.source?.kind === 'request' ? (
<p className="text-muted-foreground text-xs leading-relaxed">
{limitSettledNote(slug)}
</p>
) : null}

{phase === 'settled' && action.canContinue ? null : phase === 'settled' ? (
<p className="flex items-center gap-2 text-sm font-medium" role="status">
Sent
{explorerUrl !== undefined ? (
Expand Down
Loading