Skip to content

Commit d8703d3

Browse files
committed
Add payment rail runtime and TxODDS procurement demo
1 parent f51d29a commit d8703d3

38 files changed

Lines changed: 1486 additions & 19 deletions

SKILLS.md

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,28 @@
1-
# Skills Solana dev
1+
# Skills - Solana Dev
22

3-
An optional Claude Code skill set that adds commands and knowledge for Solana developmenthandy if you
4-
fork or extend the escrow contract.
3+
An optional Claude Code skill set that adds commands and knowledge for Solana development, handy if
4+
you fork or extend the escrow contract.
55

6-
## Solana dev skill
6+
## Solana Dev Skill
77

88
```sh
99
npx skills add https://github.com/solana-foundation/solana-dev-skill --global --yes
1010
```
1111

12-
Installed via the [`skills`](https://github.com/vercel-labs/skills) CLI. Adds Solana knowledge +
13-
tooling: `@solana/kit`, Anchor & Pinocchio programs, LiteSVM/Mollusk/Surfpool testing, Codama client
14-
generation, Token-2022, Solana Pay, and a security checklist.
12+
Installed via the [`skills`](https://github.com/vercel-labs/skills) CLI. Adds Solana knowledge and
13+
tooling: `@solana/kit`, Anchor and Pinocchio programs, LiteSVM/Mollusk/Surfpool testing, Codama
14+
client generation, Token-2022, Solana Pay, and a security checklist.
1515

16-
Where it helped in this repo: the **escrow program** (`examples/txodds/escrow/`) written,
16+
Where it helped in this repo: the escrow program (`examples/txodds/escrow/`) was written and
1717
security-reviewed against the skill's checklist (`init` not `init_if_needed`, per-order PDA seeds,
18-
`has_one`, `close = buyer`, checked math), built, **deployed to devnet**, and tested. That's the
19-
worked example of what this skill is for.
18+
`has_one`, `close = buyer`, checked math), built, deployed to devnet, and tested. That's the worked
19+
example of what this skill is for.
2020

21-
> The skill is what you'd reach for to take a fork further — accept USDC (Token-2022), add an arbiter
22-
> to the escrow, or generate typed clients with Codama.
21+
The skill is what you'd reach for to take a fork further: accept USDC (Token-2022), add an arbiter to
22+
the escrow, or generate typed clients with Codama.
23+
24+
## Solana Agent Commerce Skill
25+
26+
This repo also includes `skills/solana-agent-commerce/`, a local builder skill for extending the
27+
market with paid services, seller personas, payment rails, verifier gates, Pay.sh procurement, x402
28+
endpoints, USDC settlement, and agent-commerce examples.

examples/txodds/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,18 @@ On the board you can also click **Pay with Phantom / Solflare** to buy the read
7070
**Solana Pay** reference-tagged transfer from your wallet to the seller, verified on-chain by the proxy
7171
(`/api/pay-intent` + `/api/pay-verify`). Needs a Devnet-funded wallet.
7272

73+
The board also includes **Run Pay.sh procurement demo**. That route calls
74+
`/api/pay-sh-edge`, has the seller buy simulated upstream context through the shared
75+
`payment-runtime` Pay.sh rail, writes `PAYMENT_REQUIRED -> PAYMENT_PROOF -> PAYMENT_CONFIRMED` into
76+
the run transcript, then settles the buyer payment through the same escrow path. Each run folder gets
77+
a `procurement.json` receipt beside the delivered read and settlement files.
78+
79+
**Every settle leaves a run** — agent-settled or wallet-paid, the proxy persists it to a run ledger
80+
(`data/txodds-runs/`, session `web-oracle`): the read, the order-bound reference, the txs. The UI's
81+
**Runs panel** lists them (`/api/runs`, `/api/run?runId=`), and **Grade runs** (`/api/grade-runs`)
82+
checks each settled read against the **actual match result** once the fixture resolves, writing
83+
`outcome.json` into the run folder — paid reads graded against reality, not just delivered.
84+
7385
## CoralOS round (the multi-agent view)
7486

7587
The web demo above is one agent. For the **multi-agent** version - a buyer agent + a World Cup seller
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { procureTxOddsContext } from './procurement.js'
3+
4+
describe('procureTxOddsContext', () => {
5+
it('creates a Pay.sh request, verifies a receipt, and emits market payment messages', async () => {
6+
const result = await procureTxOddsContext({
7+
orderId: 'txodds-pay-sh-1',
8+
round: 7,
9+
fixtureId: '9001',
10+
buyer: 'seller-agent',
11+
seller: 'pay.sh/txodds-context',
12+
amount: '0.03',
13+
})
14+
expect(result.request.rail).toBe('pay-sh')
15+
expect(result.verification.paid).toBe(true)
16+
expect(result.verification.proof).toMatch(/^pay-sh-demo:/)
17+
expect(result.messages[0]).toContain('PAYMENT_REQUIRED round=7 rail=pay-sh')
18+
expect(result.messages[1]).toContain('PAYMENT_PROOF round=7 rail=pay-sh')
19+
expect(result.messages[2]).toContain('PAYMENT_CONFIRMED round=7 rail=pay-sh')
20+
})
21+
})
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { createHash } from 'node:crypto'
2+
import {
3+
type PaymentRequest,
4+
type PaymentVerification,
5+
} from '../../../packages/payment-runtime/src/types.js'
6+
import { PaymentRailRouter } from '../../../packages/payment-runtime/src/rail-router.js'
7+
import { payShRail } from '../../../packages/payment-runtime/src/rails/pay-sh.js'
8+
import {
9+
formatPaymentConfirmed,
10+
formatPaymentProof,
11+
formatPaymentRequired,
12+
} from '../../../packages/agent-runtime/src/market/protocol.js'
13+
14+
export interface PayShProcurement {
15+
provider: string
16+
service: string
17+
request: PaymentRequest
18+
verification: PaymentVerification
19+
messages: string[]
20+
}
21+
22+
export interface PayShProcurementInput {
23+
orderId: string
24+
round: number
25+
fixtureId: string
26+
buyer: string
27+
seller: string
28+
amount: string
29+
currency?: 'USDC'
30+
provider?: string
31+
}
32+
33+
export async function procureTxOddsContext(input: PayShProcurementInput): Promise<PayShProcurement> {
34+
const provider = input.provider ?? 'pay.sh/txodds-context'
35+
const router = new PaymentRailRouter([
36+
payShRail({ providerAllowlist: [provider], catalogBaseUrl: 'https://pay.sh/api' }),
37+
])
38+
const order = {
39+
id: input.orderId,
40+
round: input.round,
41+
service: 'txline-edge-upstream',
42+
buyer: input.buyer,
43+
seller: input.seller,
44+
amount: input.amount,
45+
currency: input.currency ?? 'USDC',
46+
rail: 'pay-sh' as const,
47+
metadata: {
48+
provider,
49+
fixtureId: input.fixtureId,
50+
url: `https://pay.sh/api/quicknode/rpc?fixtureId=${encodeURIComponent(input.fixtureId)}`,
51+
},
52+
}
53+
const request = await router.requestPayment(order)
54+
const receipt = receiptFor({ orderId: input.orderId, fixtureId: input.fixtureId, provider, amount: input.amount })
55+
const verification = await router.verifyPayment({
56+
...request,
57+
metadata: { ...request.metadata, payShReceipt: receipt },
58+
})
59+
const reference = request.reference ?? request.orderId
60+
return {
61+
provider,
62+
service: order.service,
63+
request,
64+
verification,
65+
messages: [
66+
formatPaymentRequired({
67+
round: input.round,
68+
rail: 'pay-sh',
69+
amount: request.amount,
70+
currency: request.currency,
71+
reference,
72+
seller: input.seller,
73+
...(request.url ? { url: request.url } : {}),
74+
}),
75+
formatPaymentProof({
76+
round: input.round,
77+
rail: 'pay-sh',
78+
reference,
79+
proof: receipt,
80+
buyer: input.buyer,
81+
}),
82+
formatPaymentConfirmed({
83+
round: input.round,
84+
rail: 'pay-sh',
85+
reference,
86+
paid: verification.paid,
87+
amount: verification.amount,
88+
currency: verification.currency,
89+
}),
90+
],
91+
}
92+
}
93+
94+
function receiptFor(input: { orderId: string; fixtureId: string; provider: string; amount: string }): string {
95+
return `pay-sh-demo:${createHash('sha256')
96+
.update(`${input.orderId}:${input.fixtureId}:${input.provider}:${input.amount}`)
97+
.digest('hex')}`
98+
}

examples/txodds/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"dependencies": {
1818
"@coral-xyz/anchor": "^0.32.1",
1919
"@pay/agent-runtime": "file:../../packages/agent-runtime",
20+
"@pay/payment-runtime": "file:../../packages/payment-runtime",
2021
"@solana/spl-token": "^0.4.14",
2122
"@solana/web3.js": "^1.98.4",
2223
"axios": "^1.11.0",

examples/txodds/server/proxy.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
} from '@pay/agent-runtime'
3333
import type { RunRecord, TranscriptEntry, TxEntry } from '@pay/agent-runtime'
3434
import { analyzeEdge, fairLine } from '../agent/edge.js'
35+
import { procureTxOddsContext, type PayShProcurement } from '../agent/procurement.js'
3536
import {
3637
makeArbiter, initConfig, open as arbiterOpen, arbitrateRelease,
3738
configPda, vaultPda, arbitratedEscrowPda, ARBITER_PROGRAM_ID,
@@ -257,6 +258,7 @@ function persistWebRun(args: {
257258
amountSol: number
258259
fixtureId: string
259260
payVerify?: any
261+
procurement?: PayShProcurement
260262
}): any {
261263
const round = latestRound() + 1
262264
const now = new Date().toISOString()
@@ -283,18 +285,24 @@ function persistWebRun(args: {
283285
deposit: { sig: args.settle.open?.sig ?? args.settle.deposit?.sig ?? args.payVerify?.sig ?? '', buyer: args.settle.buyer ?? 'browser-wallet' },
284286
} : undefined,
285287
delivery: { raw: rawDelivery, data: args.read.delivery, sha256: sha256Hex(rawDelivery) },
286-
verification: { verdict: deliveredOk(args.read.delivery) ? 'pass' : 'fail', checked: 'delivery-present' },
288+
verification: {
289+
verdict: deliveredOk(args.read.delivery) ? 'pass' : 'fail',
290+
checked: 'delivery-present',
291+
...(args.procurement ? { upstreamPayment: args.procurement.verification } : {}),
292+
},
287293
txs,
288294
updatedAt: now,
289295
}
290296
const transcript: TranscriptEntry[] = [
291297
{ sender: 'web', text: JSON.stringify(args.read.request) },
298+
...(args.procurement?.messages.map((text) => ({ sender: 'seller-agent', text })) ?? []),
292299
{ sender: 'oracle', text: rawDelivery },
293300
{ sender: 'settlement', text: JSON.stringify(args.settle ?? args.payVerify) },
294301
]
295302
const dir = writeRun(RUNS_DIR, run, transcript)
296303
fs.writeFileSync(join(dir, 'read_request.json'), JSON.stringify(args.read.request, null, 2) + '\n', 'utf8')
297304
fs.writeFileSync(join(dir, 'delivered_read.json'), JSON.stringify(args.read.delivery, null, 2) + '\n', 'utf8')
305+
if (args.procurement) fs.writeFileSync(join(dir, 'procurement.json'), JSON.stringify(args.procurement, null, 2) + '\n', 'utf8')
298306
return { ...args.settle, runId: run.runId, runDir: dir }
299307
}
300308

@@ -508,6 +516,40 @@ http
508516
}
509517
result = persistWebRun({ read, settle: result, amountSol: amount, fixtureId })
510518
res.end(JSON.stringify(result))
519+
} else if (url.pathname === '/api/pay-sh-edge') {
520+
// Seller-style demo: before delivering, the TxODDS seller buys upstream context through the
521+
// payment-runtime Pay.sh rail, records the receipt, then settles the buyer payment as usual.
522+
const amount = Number(url.searchParams.get('amount') ?? '0.001')
523+
const upstreamUsdc = url.searchParams.get('upstreamUsdc') ?? '0.03'
524+
const fixtureId = url.searchParams.get('fixtureId') ?? ''
525+
const round = latestRound() + 1
526+
const read = latestRead.get(fixtureId) ?? await readEdge(fixtureId)
527+
const procurement = await procureTxOddsContext({
528+
orderId: `${WEB_SESSION}/round-${round}/pay-sh`,
529+
round,
530+
fixtureId,
531+
buyer: 'txodds-seller-agent',
532+
seller: 'pay.sh/txodds-context',
533+
amount: upstreamUsdc,
534+
})
535+
let result: any
536+
if (arbiterKeypair()) {
537+
try { result = await settleViaArbiter(amount, fixtureId, read, round) }
538+
catch (e) { result = await settle(amount, fixtureId, read, round); result.arbiterError = (e as Error).message }
539+
} else {
540+
result = await settle(amount, fixtureId, read, round)
541+
}
542+
result = persistWebRun({ read, settle: result, amountSol: amount, fixtureId, procurement })
543+
res.end(JSON.stringify({
544+
...result,
545+
procurement: {
546+
provider: procurement.provider,
547+
amount: procurement.request.amount,
548+
currency: procurement.request.currency,
549+
paid: procurement.verification.paid,
550+
proof: procurement.verification.proof,
551+
},
552+
}))
511553
} else if (url.pathname === '/api/runs') {
512554
res.end(JSON.stringify(listRuns(RUNS_DIR).filter((r) => r.session === WEB_SESSION).sort((a, b) => b.round - a.round)))
513555
} else if (url.pathname === '/api/run') {

examples/txodds/web/app.js

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ function EdgeCard({ edge }) {
208208
}
209209

210210
// the explainer - what the app actually does, end to end, threaded with the selected fixture's numbers
211-
function Pipeline({ edge, source, settleRes }) {
211+
function Pipeline({ edge, source, settleRes, procurementRes }) {
212212
const fav = edge?.fair?.favourite
213213
const steps = [
214214
{ n: 1, title: 'Verified data',
@@ -220,6 +220,9 @@ function Pipeline({ edge, source, settleRes }) {
220220
{ n: 3, title: 'Settled by a neutral arbiter',
221221
desc: 'The buyer funds a per-order escrow but cannot unilaterally refund - a trusted neutral arbiter program releases to the seller on verified delivery. The escrow reference is bound to the read (sha256), so the on-chain order IS the data bought. Real devnet txs, linked on Explorer.',
222222
live: settleRes?.ok ? `${settleRes.amountSol} SOL${settleRes.mode === 'arbiter' ? ' - arbiter' : ''}` : `${SETTLE_SOL} SOL` },
223+
{ n: 4, title: 'Optional upstream spend',
224+
desc: 'The seller can buy an upstream API through the Pay.sh rail before delivery. The run ledger stores PAYMENT_REQUIRED, PAYMENT_PROOF, and PAYMENT_CONFIRMED alongside the escrow settlement.',
225+
live: procurementRes?.procurement?.paid ? `${procurementRes.procurement.amount} ${procurementRes.procurement.currency}` : 'Pay.sh' },
223226
]
224227
return html`
225228
<section class="pipeline">
@@ -230,7 +233,7 @@ function Pipeline({ edge, source, settleRes }) {
230233
<div class="pipe-h"><span class="pipe-n">0${s.n}</span><span class="pipe-live">${s.live}</span></div>
231234
<h4>${s.title}</h4>
232235
<p>${s.desc}</p>
233-
${i < 2 && html`<span class="pipe-arrow">-></span>`}
236+
${i < steps.length - 1 && html`<span class="pipe-arrow">-></span>`}
234237
</div>`)}
235238
</div>
236239
</section>`
@@ -288,6 +291,21 @@ function SettleResult({ r }) {
288291
<a href=${addrLink(ESCROW_PROGRAM)} target="_blank" rel="noreferrer">escrow program open</a></div>`
289292
}
290293

294+
function ProcurementResult({ r }) {
295+
if (!r) return null
296+
if (r.ok) return html`
297+
<div class="settled ok pay-sh-result">
298+
<div class="settled-line"><span class="bind-tag">Pay.sh</span>
299+
seller procured upstream context for <b>${r.procurement?.amount} ${r.procurement?.currency}</b>
300+
then settled <b>${r.amountSol} SOL</b> to the seller
301+
</div>
302+
<div class="settled-line bind">
303+
proof <b>${shortAddr(r.procurement?.proof)}</b> stored in the run ledger as PAYMENT_PROOF
304+
</div>
305+
</div>`
306+
return html`<div class="settled sim">Pay.sh demo unavailable${r.error ? ` (${String(r.error).slice(0, 70)})` : ''}</div>`
307+
}
308+
291309
function RunsPanel({ runs, selectedRun, onSelect, onGrade, grading }) {
292310
const top = selectedRun ?? runs?.[0]
293311
const outcome = top?.outcome
@@ -324,6 +342,7 @@ function RunsPanel({ runs, selectedRun, onSelect, onGrade, grading }) {
324342
<span>request <b>${top.want?.arg}</b></span>
325343
${top.delivery?.sha256 && html`<span>delivery sha <b>${shortAddr(top.delivery.sha256)}</b></span>`}
326344
${top.escrow?.reference && html`<span>escrow ref <b>${shortAddr(top.escrow.reference)}</b></span>`}
345+
${top.verification?.upstreamPayment?.rail && html`<span>upstream <b>${top.verification.upstreamPayment.rail} ${top.verification.upstreamPayment.amount} ${top.verification.upstreamPayment.currency}</b></span>`}
327346
${outcome && html`<span>reality <b>${outcome.status === 'graded'
328347
? `${outcome.actual?.winner ?? 'unknown'} - ${outcome.correct === true ? 'hit' : outcome.correct === false ? 'miss' : 'unscored'}`
329348
: 'pending'}</b></span>`}
@@ -398,6 +417,8 @@ function App() {
398417
const [loadingOdds, setLoadingOdds] = useState(false)
399418
const [edge, setEdge] = useState(null)
400419
const [settleRes, setSettleRes] = useState(null)
420+
const [procurementRes, setProcurementRes] = useState(null)
421+
const [procuring, setProcuring] = useState(false)
401422
const [settling, setSettling] = useState(false)
402423
const [runs, setRuns] = useState(null)
403424
const [selectedRun, setSelectedRun] = useState(null)
@@ -423,6 +444,21 @@ function App() {
423444
finally { setGrading(false) }
424445
}
425446

447+
const runPayShDemo = async () => {
448+
if (!selected || source !== 'live') return
449+
setProcuring(true)
450+
setProcurementRes(null)
451+
try {
452+
const r = await (await fetch(`${PROXY}/api/pay-sh-edge?fixtureId=${selected.FixtureId}&amount=${SETTLE_SOL}&upstreamUsdc=0.03`)).json()
453+
setProcurementRes(r)
454+
await loadRuns()
455+
} catch (e) {
456+
setProcurementRes({ ok: false, error: String(e?.message ?? e) })
457+
} finally {
458+
setProcuring(false)
459+
}
460+
}
461+
426462
// load the board: fixtures with verified live odds (inlined). The free World Cup tier's odds are
427463
// intermittent and the proxy needs a few seconds to subscribe on a cold start, so we KEEP polling
428464
// until live data arrives - showing the labelled sample board meanwhile, then switching to live on
@@ -486,7 +522,7 @@ function App() {
486522
useEffect(() => {
487523
if (!selected) return
488524
let alive = true
489-
setEdge(null); setSettleRes(null); setSettling(false)
525+
setEdge(null); setSettleRes(null); setProcurementRes(null); setSettling(false)
490526
;(async () => {
491527
// 1) the agent's call
492528
let e = clientEdge(selected)
@@ -525,7 +561,7 @@ function App() {
525561
turns it into <b>fair (break-even) odds + a plain read</b>, and gets paid through an on-chain escrow.</p>
526562
</header>
527563
<main>
528-
<${Pipeline} edge=${edge} source=${source} settleRes=${settleRes} />
564+
<${Pipeline} edge=${edge} source=${source} settleRes=${settleRes} procurementRes=${procurementRes} />
529565
${!fixtures && html`<p class="muted" style=${{ textAlign: 'center' }}>loading fixtures...</p>`}
530566
${selected && html`
531567
<section class="featured">
@@ -546,6 +582,10 @@ function App() {
546582
<span class="spin"></span> agent delivered - arbiter settling ${SETTLE_SOL} SOL in escrow on devnet...
547583
</div>`}
548584
${settleRes && html`<${SettleResult} r=${settleRes} />`}
585+
<button class="pay-sh-btn" disabled=${source !== 'live' || procuring} onClick=${runPayShDemo}>
586+
${procuring ? html`<span class="spin"></span> procuring + settling...` : 'Run Pay.sh procurement demo'}
587+
</button>
588+
${procurementRes && html`<${ProcurementResult} r=${procurementRes} />`}
549589
${selected && html`<${PayButton} fixture=${selected} />`}
550590
</div>
551591
</div>

0 commit comments

Comments
 (0)