@perkos/agent-sdk 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/README.md +21 -5
- package/SECURITY.md +12 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/x402-direct.d.ts +16 -0
- package/dist/x402-direct.d.ts.map +1 -1
- package/dist/x402-direct.js +43 -26
- package/dist/x402-direct.js.map +1 -1
- package/dist/x402-paying.d.ts +193 -0
- package/dist/x402-paying.d.ts.map +1 -0
- package/dist/x402-paying.js +587 -0
- package/dist/x402-paying.js.map +1 -0
- package/docs/X402_PAYMENTS.md +143 -0
- package/docs/plans/2026-08-27-x402-paying-flow-design.md +125 -0
- package/examples/x402-payment-intent.ts +81 -0
- package/package.json +3 -1
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# Paying Nayori x402 resources
|
|
2
|
+
|
|
3
|
+
Nayori's payer flow prepares a standard, non-sponsored Stacks transaction for direct STX, sBTC,
|
|
4
|
+
or USDCx payment. It binds the payment to the exact HTTP request and returns a body suitable for the
|
|
5
|
+
hosted facilitator. The SDK does not broadcast the transaction.
|
|
6
|
+
|
|
7
|
+
The public Nayori Platform deployment is currently quote-only. Payment verification, settlement,
|
|
8
|
+
reconciliation, and delivery remain runtime-disabled until the testnet release gates and external
|
|
9
|
+
review are complete. The APIs below are developer foundations, not a claim that production
|
|
10
|
+
settlement is live.
|
|
11
|
+
|
|
12
|
+
## Safe offline quickstart
|
|
13
|
+
|
|
14
|
+
The included quickstart creates a testnet USDCx quote, deterministic payment intent, policy
|
|
15
|
+
reservation, and canonical unsigned transaction. It contains no private key, requests no wallet
|
|
16
|
+
approval, performs no network call, and releases the reservation without signing:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm run quickstart:x402:payer
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Shared payment policy
|
|
23
|
+
|
|
24
|
+
Both interactive and automated signers use the same mandatory, fail-closed policy:
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { NayoriX402PaymentPolicy } from "@perkos/agent-sdk";
|
|
28
|
+
|
|
29
|
+
const policy = new NayoriX402PaymentPolicy({
|
|
30
|
+
allowedNetworks: ["mainnet"],
|
|
31
|
+
allowedAssets: ["sbtc", "usdcx"],
|
|
32
|
+
allowedRecipients: [merchantStacksAddress],
|
|
33
|
+
allowedOrigins: ["https://merchant.example"],
|
|
34
|
+
allowedMerchantIds: ["merchant-research"],
|
|
35
|
+
maxPerTransaction: { sbtc: 25_000n, usdcx: 5_000_000n },
|
|
36
|
+
maxPerSession: { sbtc: 100_000n, usdcx: 20_000_000n },
|
|
37
|
+
maxFeePerTransaction: 5_000n,
|
|
38
|
+
maxFeePerSession: 25_000n,
|
|
39
|
+
minQuoteValiditySeconds: 30,
|
|
40
|
+
});
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
All values are atomic units: micro-STX, satoshis, or 10^-6 USDCx. Fee limits are micro-STX.
|
|
44
|
+
Authorization reserves amount and fee before asynchronous signing. A valid signature commits that
|
|
45
|
+
usage even if settlement later becomes ambiguous. Failed or cancelled signing releases it. Active
|
|
46
|
+
reservations count toward session limits, so concurrent agents cannot oversubscribe the budget.
|
|
47
|
+
|
|
48
|
+
## Interactive Leather signing
|
|
49
|
+
|
|
50
|
+
Connect the wallet with the official `@stacks/connect` package and select its STX address and
|
|
51
|
+
compressed public key. Pass the package's `request` function to `LeatherSigner`:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import { connect, request } from "@stacks/connect";
|
|
55
|
+
import {
|
|
56
|
+
LeatherSigner,
|
|
57
|
+
NayoriX402PaymentClient,
|
|
58
|
+
} from "@perkos/agent-sdk";
|
|
59
|
+
|
|
60
|
+
const connection = await connect();
|
|
61
|
+
const account = connection.addresses.find(
|
|
62
|
+
(candidate) => candidate.symbol === "STX" || candidate.address.startsWith("S")
|
|
63
|
+
);
|
|
64
|
+
if (!account) throw new Error("The wallet did not return a Stacks account");
|
|
65
|
+
|
|
66
|
+
const signer = new LeatherSigner({
|
|
67
|
+
network: "mainnet",
|
|
68
|
+
address: account.address,
|
|
69
|
+
publicKey: account.publicKey,
|
|
70
|
+
request,
|
|
71
|
+
});
|
|
72
|
+
const payer = new NayoriX402PaymentClient({ signer, policy });
|
|
73
|
+
|
|
74
|
+
const prepared = await payer.preparePayment({
|
|
75
|
+
signedQuote: quoteResponse.signedQuote,
|
|
76
|
+
quote: quoteResponse.quote,
|
|
77
|
+
paymentRequirements: quoteResponse.paymentRequirements,
|
|
78
|
+
request: protectedRequest,
|
|
79
|
+
fee: quotedFeeMicroStx,
|
|
80
|
+
nonce: currentAccountNonce,
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`LeatherSigner` calls only `stx_signTransaction` with `broadcast: false`. It rejects a wallet result
|
|
85
|
+
that contains only a txid because the Nayori facilitator must receive, reserve, and broadcast the
|
|
86
|
+
signed bytes exactly once.
|
|
87
|
+
|
|
88
|
+
## Automated agent signing
|
|
89
|
+
|
|
90
|
+
`PolicySigner` delegates the cryptographic operation to an application-owned service. The callback
|
|
91
|
+
receives the immutable intent and unsigned transaction, never a key:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
import {
|
|
95
|
+
NayoriX402PaymentClient,
|
|
96
|
+
PolicySigner,
|
|
97
|
+
} from "@perkos/agent-sdk";
|
|
98
|
+
|
|
99
|
+
const signer = new PolicySigner({
|
|
100
|
+
network: "mainnet",
|
|
101
|
+
address: agentStacksAddress,
|
|
102
|
+
publicKey: agentCompressedPublicKey,
|
|
103
|
+
sign: async ({ intent, transaction }) => {
|
|
104
|
+
const response = await fetch("https://signer.internal/v1/stacks/sign", {
|
|
105
|
+
method: "POST",
|
|
106
|
+
headers: { "content-type": "application/json" },
|
|
107
|
+
body: JSON.stringify({ intent, transaction }),
|
|
108
|
+
});
|
|
109
|
+
if (!response.ok) throw new Error("The custody policy denied signing");
|
|
110
|
+
return response.json() as Promise<{ transaction: string }>;
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
const payer = new NayoriX402PaymentClient({ signer, policy });
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Keep the signing service on a private, mutually authenticated boundary. It must independently
|
|
117
|
+
validate the intent, enforce its own durable limits and nonce policy, and call a KMS/HSM or isolated
|
|
118
|
+
wallet. Do not send a private key to the SDK callback or load one into an LLM/agent process. The
|
|
119
|
+
in-process SDK policy is defense in depth; the custody service is the final authorization boundary.
|
|
120
|
+
|
|
121
|
+
## Submit to the facilitator
|
|
122
|
+
|
|
123
|
+
After local verification, `prepared.settlementRequest` is exactly the JSON body expected by
|
|
124
|
+
`POST /v1/x402/settle`:
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
const response = await fetch(`${facilitatorOrigin}/v1/x402/settle`, {
|
|
128
|
+
method: "POST",
|
|
129
|
+
headers: {
|
|
130
|
+
authorization: `Bearer ${merchantCredential}`,
|
|
131
|
+
"content-type": "application/json",
|
|
132
|
+
},
|
|
133
|
+
body: JSON.stringify(prepared.settlementRequest),
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
The merchant credential belongs to the protected resource or its trusted backend, not to the payer
|
|
138
|
+
agent. In the normal x402 exchange, the payer returns its payment payload to the resource server and
|
|
139
|
+
that server calls or proxies the authenticated facilitator operation.
|
|
140
|
+
|
|
141
|
+
Before releasing a resource, require confirmed settlement and a valid signed settlement receipt.
|
|
142
|
+
Do not treat wallet approval, a local txid, a broadcast response, or an unconfirmed mempool entry as
|
|
143
|
+
completed payment.
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Nayori x402 paying flow design
|
|
2
|
+
|
|
3
|
+
Date: 2026-08-27
|
|
4
|
+
|
|
5
|
+
Status: approved for implementation
|
|
6
|
+
|
|
7
|
+
## Objective
|
|
8
|
+
|
|
9
|
+
Add a payer-side TypeScript flow for Nayori's direct Stacks x402 profile. The same immutable
|
|
10
|
+
payment intent must support an interactive Leather wallet and an automated agent whose key stays
|
|
11
|
+
inside an application-owned KMS, HSM, secret manager, or wallet service. The SDK prepares and
|
|
12
|
+
validates the payment; it does not broadcast. The Nayori facilitator remains the only component
|
|
13
|
+
authorized to reserve and broadcast a submitted payment.
|
|
14
|
+
|
|
15
|
+
This increment does not modify or redeploy the approved Milestone 1 contracts, enable Platform
|
|
16
|
+
settlement, or claim Milestone 2 transaction/adoption evidence.
|
|
17
|
+
|
|
18
|
+
## Constraints and external behavior
|
|
19
|
+
|
|
20
|
+
- Direct payments use the existing `stacks-signed-tx-v1` profile for STX, sBTC, and USDCx.
|
|
21
|
+
- `PaymentIntent` is deterministic, serializable, request-bound, and contains no private material.
|
|
22
|
+
- The SDK must validate the trusted quote, accepted x402 requirement, request digest, payer,
|
|
23
|
+
recipient, asset, atomic amount, fee, nonce, signature, memo, and post-conditions before returning
|
|
24
|
+
settlement input.
|
|
25
|
+
- A signer receives only an immutable intent and the canonical unsigned transaction.
|
|
26
|
+
- Leather integration uses the current Stacks Connect `stx_signTransaction` method with
|
|
27
|
+
`broadcast: false`; a missing signed transaction is a failure even if a wallet returns a txid.
|
|
28
|
+
- Headless integration delegates signing to an application callback. The SDK never accepts,
|
|
29
|
+
requests, stores, or logs the agent's private key.
|
|
30
|
+
- Spending policy is mandatory and fail-closed. An LLM or agent cannot override its decision.
|
|
31
|
+
- Session limits count signed authorizations, not confirmed settlements. This conservative rule
|
|
32
|
+
prevents repeated signing after an ambiguous network result.
|
|
33
|
+
|
|
34
|
+
## Public API
|
|
35
|
+
|
|
36
|
+
The new module exports:
|
|
37
|
+
|
|
38
|
+
- `NayoriX402PaymentIntent` and `createNayoriX402PaymentIntent`;
|
|
39
|
+
- `NayoriX402PaymentPolicy`, its explicit configuration, and read-only session usage;
|
|
40
|
+
- `NayoriX402PaymentSigner` as the common signer contract;
|
|
41
|
+
- `LeatherSigner`, a Stacks Connect callback adapter;
|
|
42
|
+
- `PolicySigner`, a remote/headless signing callback adapter;
|
|
43
|
+
- `NayoriX402PaymentClient`, which builds, authorizes, signs, verifies, and returns the exact input
|
|
44
|
+
accepted by the hosted `/v1/x402/settle` route.
|
|
45
|
+
|
|
46
|
+
The client input contains the signed quote bundle returned by Nayori, the protected request, fee,
|
|
47
|
+
and nonce. Address and compressed public key come from the signer. The output contains
|
|
48
|
+
`signedQuote`, `paymentRequirements`, `paymentPayload`, `request`, and locally verified payment
|
|
49
|
+
metadata. It never broadcasts.
|
|
50
|
+
|
|
51
|
+
## Intent and transaction construction
|
|
52
|
+
|
|
53
|
+
Intent construction first normalizes the quote and proves that:
|
|
54
|
+
|
|
55
|
+
1. the protected method, canonical URL, and body digest match the quote;
|
|
56
|
+
2. the accepted x402 requirement is exactly the requirement derived from that quote;
|
|
57
|
+
3. the quote is currently valid with the configured clock-skew allowance;
|
|
58
|
+
4. the signer's compressed public key derives the declared Stacks payer address;
|
|
59
|
+
5. fee and nonce are canonical unsigned integers.
|
|
60
|
+
|
|
61
|
+
The intent ID is a SHA-256 digest over a domain-separated canonical representation containing the
|
|
62
|
+
quote fingerprint, payer, public key, fee, and nonce. The signed transaction is constructed as:
|
|
63
|
+
|
|
64
|
+
- STX: a native token transfer with the quote fingerprint in the memo;
|
|
65
|
+
- sBTC/USDCx: the canonical SIP-010 `transfer` call with the payer and recipient principals, the
|
|
66
|
+
fingerprint in the optional memo, deny mode, and one exact fungible-token post-condition.
|
|
67
|
+
|
|
68
|
+
All three paths are standard, non-sponsored transactions. Fee and nonce are provided explicitly so
|
|
69
|
+
transaction preparation is deterministic and performs no hidden network request.
|
|
70
|
+
|
|
71
|
+
## Policy and concurrency
|
|
72
|
+
|
|
73
|
+
Policy configuration explicitly lists allowed networks, assets, recipients, and HTTPS origins,
|
|
74
|
+
plus per-transaction and per-session atomic limits for every allowed asset and a maximum fee in
|
|
75
|
+
micro-STX. Optional merchant IDs and a minimum remaining quote lifetime further narrow authority.
|
|
76
|
+
|
|
77
|
+
Authorization reserves both payment amount and fee synchronously before asynchronous signing. The
|
|
78
|
+
reservation is committed after a valid signed transaction is produced, or released if building,
|
|
79
|
+
signing, or local verification fails. Active reservations are included in session-limit checks, so
|
|
80
|
+
concurrent agent calls cannot oversubscribe the budget. A committed intent cannot be signed again
|
|
81
|
+
by the same policy instance.
|
|
82
|
+
|
|
83
|
+
## Signer boundaries
|
|
84
|
+
|
|
85
|
+
`LeatherSigner` is configured with the connected address, compressed public key, and an injected
|
|
86
|
+
Stacks Connect-compatible request function. It calls only:
|
|
87
|
+
|
|
88
|
+
```text
|
|
89
|
+
stx_signTransaction({ transaction: unsignedHex, broadcast: false })
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`PolicySigner` is configured with the same public identity and a remote callback. That callback may
|
|
93
|
+
talk to a KMS/HSM/wallet service and must return a fully signed serialized transaction. No example
|
|
94
|
+
introduced for this payer flow uses an environment variable or local raw private key. The older,
|
|
95
|
+
explicitly opt-in escrow lifecycle example remains separate from this x402 custody boundary.
|
|
96
|
+
|
|
97
|
+
Neither signer result is trusted. The client runs the existing pure x402 verifier and additionally
|
|
98
|
+
checks payer, nonce, fee, non-sponsored authorization, and the canonical intent fields before
|
|
99
|
+
committing policy usage.
|
|
100
|
+
|
|
101
|
+
## Errors and observability
|
|
102
|
+
|
|
103
|
+
Invalid quote context returns `X402_INVALID`; policy rejection returns the existing `POLICY_DENIED`
|
|
104
|
+
or `POLICY_LIMIT_REQUIRED`; signer cancellation or malformed output returns `SIGNING_FAILED`.
|
|
105
|
+
Errors include stable, non-secret context such as intent ID, asset, or quote ID. Signed quote tokens,
|
|
106
|
+
request bodies, unsigned/signed transaction bytes, and callbacks are not included in error details.
|
|
107
|
+
|
|
108
|
+
## Tests and release gate
|
|
109
|
+
|
|
110
|
+
Regression coverage must include all three assets, Leather `broadcast: false`, remote signing,
|
|
111
|
+
request/requirement mismatch, public-key mismatch, fee and amount limits, recipient/origin denial,
|
|
112
|
+
quote expiry, signer mutation, invalid signatures, cancellation, concurrent reservation, release on
|
|
113
|
+
failure, and duplicate-intent prevention. `npm run verify`, `npm audit --audit-level=high`, package
|
|
114
|
+
contents inspection, and a clean-room consumer import are required before the PR is opened.
|
|
115
|
+
|
|
116
|
+
## Primary references
|
|
117
|
+
|
|
118
|
+
- Stacks Connect `request` API and normalized `stx_signTransaction` result:
|
|
119
|
+
https://docs.stacks.co/reference/stacks.js/stacks-connect/request/request
|
|
120
|
+
- Stacks Connect wallet compatibility, including Leather support:
|
|
121
|
+
https://docs.stacks.co/stacks-connect/wallet-support
|
|
122
|
+
- Stacks transaction construction and unsigned transaction primitives:
|
|
123
|
+
https://docs.stacks.co/stacks.js/build-transactions
|
|
124
|
+
- Stacks node raw transaction broadcast boundary:
|
|
125
|
+
https://docs.stacks.co/reference/api/stacks-node-rpc/transactions
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import {
|
|
2
|
+
NayoriX402PaymentPolicy,
|
|
3
|
+
buildNayoriX402UnsignedPaymentTransaction,
|
|
4
|
+
createNayoriX402PaymentIntent,
|
|
5
|
+
createNayoriX402PaymentRequirements,
|
|
6
|
+
createNayoriX402Quote,
|
|
7
|
+
} from "@perkos/agent-sdk";
|
|
8
|
+
|
|
9
|
+
const payer = "ST1THWXQ8368SDN2MJGE4BMDKMCHZ2GSVTSQDA7QF";
|
|
10
|
+
const payerPublicKey =
|
|
11
|
+
"0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798";
|
|
12
|
+
const payTo = "ST3AZN3BSQYJ5VWMNG92N88Z4G9498VYSHDZD9EK";
|
|
13
|
+
const now = Math.floor(Date.now() / 1_000);
|
|
14
|
+
const request = {
|
|
15
|
+
method: "POST",
|
|
16
|
+
url: "https://api.example.com/v1/weather",
|
|
17
|
+
body: JSON.stringify({ city: "Miami" }),
|
|
18
|
+
} as const;
|
|
19
|
+
|
|
20
|
+
const quote = await createNayoriX402Quote({
|
|
21
|
+
quoteId: "quote-payer-quickstart",
|
|
22
|
+
merchantId: "merchant-weather",
|
|
23
|
+
network: "testnet",
|
|
24
|
+
asset: "usdcx",
|
|
25
|
+
amount: 100_000n,
|
|
26
|
+
payTo,
|
|
27
|
+
...request,
|
|
28
|
+
issuedAt: now,
|
|
29
|
+
expiresAt: now + 300,
|
|
30
|
+
});
|
|
31
|
+
const paymentRequirements = await createNayoriX402PaymentRequirements(quote);
|
|
32
|
+
const intent = await createNayoriX402PaymentIntent({
|
|
33
|
+
quote,
|
|
34
|
+
paymentRequirements,
|
|
35
|
+
request,
|
|
36
|
+
payer,
|
|
37
|
+
publicKey: payerPublicKey,
|
|
38
|
+
fee: 300n,
|
|
39
|
+
nonce: 7n,
|
|
40
|
+
nowSeconds: now,
|
|
41
|
+
});
|
|
42
|
+
const policy = new NayoriX402PaymentPolicy(
|
|
43
|
+
{
|
|
44
|
+
allowedNetworks: ["testnet"],
|
|
45
|
+
allowedAssets: ["usdcx"],
|
|
46
|
+
allowedRecipients: [payTo],
|
|
47
|
+
allowedOrigins: ["https://api.example.com"],
|
|
48
|
+
allowedMerchantIds: ["merchant-weather"],
|
|
49
|
+
maxPerTransaction: { usdcx: 100_000n },
|
|
50
|
+
maxPerSession: { usdcx: 500_000n },
|
|
51
|
+
maxFeePerTransaction: 500n,
|
|
52
|
+
maxFeePerSession: 2_500n,
|
|
53
|
+
},
|
|
54
|
+
() => now
|
|
55
|
+
);
|
|
56
|
+
const authorization = policy.reserve(intent);
|
|
57
|
+
const unsignedTransaction = await buildNayoriX402UnsignedPaymentTransaction(intent);
|
|
58
|
+
|
|
59
|
+
console.log(
|
|
60
|
+
JSON.stringify(
|
|
61
|
+
{
|
|
62
|
+
status: "ready-for-signature",
|
|
63
|
+
intentId: intent.intentId,
|
|
64
|
+
quoteId: intent.quoteId,
|
|
65
|
+
asset: intent.asset,
|
|
66
|
+
amount: intent.amount,
|
|
67
|
+
payer: intent.payer,
|
|
68
|
+
payTo: intent.payTo,
|
|
69
|
+
fee: intent.fee,
|
|
70
|
+
nonce: intent.nonce,
|
|
71
|
+
quoteFingerprint: intent.quoteFingerprint,
|
|
72
|
+
unsignedTransactionBytes: unsignedTransaction.length / 2,
|
|
73
|
+
policyRemaining: authorization.remainingThisSession.toString(),
|
|
74
|
+
broadcast: false,
|
|
75
|
+
},
|
|
76
|
+
null,
|
|
77
|
+
2
|
|
78
|
+
)
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
authorization.release();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@perkos/agent-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "TypeScript SDK for agent identity, escrow settlement, and reputation on Stacks.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -33,6 +33,8 @@
|
|
|
33
33
|
"quickstart:x402": "node --import tsx examples/x402-foundation.ts",
|
|
34
34
|
"prequickstart:x402:facilitator": "npm run build",
|
|
35
35
|
"quickstart:x402:facilitator": "node --import tsx examples/x402-facilitator.ts",
|
|
36
|
+
"prequickstart:x402:payer": "npm run build",
|
|
37
|
+
"quickstart:x402:payer": "node --import tsx examples/x402-payment-intent.ts",
|
|
36
38
|
"prequickstart:testnet": "npm run build",
|
|
37
39
|
"quickstart:testnet": "node --import tsx examples/testnet-lifecycle.ts"
|
|
38
40
|
},
|